Java 面试题

JVM、JDK、JRE

  • JVM:Java虚拟机(JVM)是运行Java字节码的虚拟机,JVM有针对不同系统(Windows、Linux、macOS)的特定实现,目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的JVM实现是Java语言“一次编译,随处可以运行”的关键所在。
  • JDK和JRE:JDK是Java Development Kit,它是功能齐全的Java SDK。它拥有的一切,还有编译器(javac)和工具(javadoc和jdb)。它能够创建和编译程序。JRE是Java运行时环境,他是运行已编译Java程序所需的所有内容的集合,包括Java虚拟机(JVM),Java类库,java命令和其他的一些基础构件。他不能用于创建新程序。

Oracel JDK和OpenJDK

  • OpenJDK是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全开源的。
  • OpenJDK和Oracle JDK的代码几乎相同,但是Oracle JDK比OpenJDK更稳定,有更多的类和一些错误修复。
  • 在响应性和JVM性能方面,Oracle JDK于OpenJDK相比提供了更好的性能。

Java和C++的区别

  • 都是面向对象的语言,都支持封装、继承和多态
  • Java不提供指针来直接访问内存,程序内存更加安全
  • Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承
  • Java有自动内存管理机制,不需要开发者手动释放无用内存

字符型常量和字符串常量的区别

  • 形式上:字符常量是单引号引起的一个字符;字符串常量是双引号引起的若干个字符
  • 含以上:字符串常量相当于一个整型值(ASCII值);可以参与表达式运算;字符串常量代表一个地址值,即该字符串在内存中存放位置
  • 占内存大小:字符常量只占2个字节,即char在Java中占两个字节;字符串常量占若干个字节。

构造器Constructor是否可以被override?

Constructor不能被override(重写),但是可以overload(重载),所以可以看到一个类中有多个构造函数的情况。

重载和重写的区别?

重载: 重载就是同样一个方法能够根据输入数据的不同,做出不同的处理。发生在同一个类中,方法名一致,参数的类型、个数、顺序不同,方法返回值和访问修饰符可以不同。发生在编译期,编译器会根据参数列表挑选出具体执行哪个方法。
重写: 当子类继承自父类相同的方法,输入数据一样,但是要做出有别于父类的响应时,就需要使用重写,覆盖父类的方法。重写发生在运行期,是子类对父类的方法实现过程进行重新编写。

注意:当父类方法的返回类型是void或基本数据类型时,重写时不可修改返回类型。如果返回类型是引用类型,重写时可以返回该引用类型的子类。

Java面向对象编程三大特性:封装、继承、多态

封装: 封装把一个对象的属性私有化,同时提供一些可以被外界访问的该属性的方法,如果属性不想被外界访问,不提供相应的方法即可。

继承: 继承是使用已存在的类的定义作为基础,建立新类的技术。新类中可以增加新的属性和功能,也可以使用父类的功能。通过使用继承可以很方便的复用以前的代码。

  • 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问的,只是拥有。
  • 子类可以拥有自己的属性和方法,即实现了对父类对的拓展
  • 子类可以使用自己的方法实现父类的方法

多态: 指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用,在编译时并不确定,而是在程序运行期间才确定,即一个引用变量最终会指向哪个类的示例对象,该调用该变量的方法时,具体执行哪个类的方法,是在程序运行期间,根据不同的输入或条件才确定的。

  • 在Java中有两种形式可以实现多态:继承,多个子类对同一方法的重写;接口,实现接口并覆盖接口中的统一方法。

String、StringBuffer、StringBuilder的区别

  • String类中使用final关键字修饰字符数组(java9后是byte数组)来存储字符串,private final char[],所以String对象是不可变的。
  • StringBuffer与StringBuilder都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组来存储字符串的,但是没有使用final关键字修饰,所以这两种对象都是可变的。
  • 线程安全性:String中的对象是不可变的,也就是可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder和StringBuffer的公共父类,定义了一些字符串的基本操作,如append、insert、indexOf等公共方法。StringBuilder没有对方法加同步锁,所以是非线程安全的。StringBuffer对方法加了同步锁或多调用的方法加了同步锁,所以是线程安全的
  • 性能:每次对String类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象。相同情况下使用StringBuilder相比使用StringBuffer仅能获得10%~15%左右的性能提升,但是要冒多线程不安全的风险。
  • 操作少量的数据使用String,单线程操作大量字符串,使用StringBuilder,多线程使用大量字符串,使用StringBuffer

自动装箱与拆箱

装箱:将基本类型用他们对应的引用类型包装起来
拆箱:将包装类型转换为基本数据类型

在一个静态方法内调用一个非静态成员为什么是非法的?

由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。

在Java中定义一个不做事且没有参数的构造方法的作用

Java程序执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中”没有参数的构造方法”。
如果父类中只定义了有参数的构造方法,而子类的构造方法中又没有super()来调用父类中特定的构造方法,则编译时将发生错误,因为在父类方法中找不到”没有参数的构造方法”。

接口和抽象类的区别

  • 接口的方法默认是public,所有方法在接口中不能有实现,Java8开始接口方法可以有默认实现。而抽象类可以有非抽象的方法。
  • 接口中除了static、final变量,不能有其他变量。而抽象类中则可以有。
  • 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字拓展多个接口。
  • 接口中的方法默认修饰符是public,抽象类中的方法可以有public、protected和default这些修饰符,抽象类中的方法就是为了被重写,所以不能使用private关键字修饰。
  • 从设计层面来说,抽象类是对类的抽象,是一种模板设计。接口是对行为的抽象,是一种行为规范。
  • JDK8中,接口也可以定义静态方法,可以直接通过接口名调用。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。
  • jdk9的接口允许定义私有方法。
  • 接口的变化:jdk7以前,接口中只能有常量变量和抽象方法,接口的方法必须有实现类进行实现。jdk8接口可以有默认方法和静态方法。jdk9接口中可以有私有方法和私有静态方法。

成员变量与局部变量的区别

  • 语法上:成员变量属于类,而局部变量实在方法中定义的变量或是方法的参数。成员变量可以被public、private、static修饰,而局部变量不能被这些修饰。成员变量和局部变量都能被final修饰。
  • 存储上:如果成员变量使用static修饰,那么这个变量是属于类的,如果没有用static修饰,这个变量是属于实例的。对象存储再堆内存,如果局部变量属于基本数据类型,那么存储在栈内存,如果为引用数据类型,则存放的是指向堆内存对象的引用或是指向常量池中的地址。
  • 成员变量是对象的一部分,随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  • 成员变量如果没有被赋初值,则会自动赋与其类型的默认值,而局部变量则不会自动赋值。

创建一个对象用什么运算符?对象实体与对象引用有何不同?

new运算符,new创建对象实例,对象实例存储在堆内存中,【对象引用】指向【对象实例】,对象引用存在栈内存中。一个对象引用可以指向0-1个对象实例,而一个对象实例可以有多个对象引用指向它。

一个类的构造方法的作用是什么?若一个类没有生命构造方法,能够正确的执行吗?

主要作用是完成对类对象的初始化工作。可以执行,因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。

构造方法有哪些特性?

  • 名字与类名相同
  • 没有返回值,但不能用void声明构造函数
  • 生成类的对象的时候自动执行,无需调用

静态方法和实例方法有何不同?

  • 调用静态方法时,可以使用【类名.方法名】的方式,也可以使用【对象名.方法名】的方式。而实例方法只有后面一种方式。即调用静态方法无需创建对象。
  • 静态方法在访问本类的成员时,只允许访问静态变量和静态方法。而实例方法没有此限制

对象的相等与指向他们的引用相等,两者有什么不同?

对象的相等,比的是内存中存放的内容是否相等,而引用的相等,比较的是他们指向的内存地址是否相等。

在调用子类构造方法之前会先调用父类没有参数的构造方法,目的是?

帮助子类做初始化工作

==与equals

  • ==判断的是两个对象的地址是不是相等,即判断两个对象是不是同一个对象。基本数据类型比较的是值,引用类型比较的是内存地址。
  • equals()判断的是两个对象是否相等。
  • 当类没有覆盖equals()方法,则等于==,相当于还是比较内存地址;一般,我们会覆盖equals()方法来实现比较两个类是否相等。

hashCode与equals

hashCode(): hashCode()的作用时获取哈希码,该方法定义在JDK的Object类中,这就意味着所有的类都有hashCode()函数。该函数通常是将对象的内存地址转换为int整数返回。

两个对象的hashCode相等,这两个对象一定相等吗: 不一定相等,因为存在哈希碰撞。如果两个对象相等,则hashCode一定也是相等的。

hashCode的作用: 当有对象加入HashSet时,HashSet会先计算出对象的hashCode值,将其他已经加入的对象的hashCode值做比较,如果发现有相同的hashCode(此时还不能断定两个对象完全相等),再调用equals()方法比较,如果相等,则不会被加入HashSet。有了hashCode,大大减少了equals()的次数,提高了执行速度。

为什么重写equals()方法必须重写hashCode()方法?: 两个对象相等,则hashCode一定相等,两个对象有相同的hashcode,他们也不一定相等。因此,equals()方法被覆盖,则hashCode方法也必须被覆盖。

为什么Java中只有值传递?

  • Java中总是采用按值调用,也就是说,方法得到的时参数值的拷贝,也就是说不能修改传递给它的参数的内容。
  • 方法不能修改一个基本数据类型的参数
  • 方法可以改变一个对象参数的状态
  • 方法不能让对象参数引用一个新的对象。

简述线程、程序、进程的基本概念

  • 线程与进程相似,但线程是一个比进程更小的执行单位,一个进程在执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程或者在各个线程之间切换工作时,负担要比进程小得多。线程也被称为轻量级进程
  • 程序时含有执行和数据的文件,被存储在磁盘或其他数据存储设备中,也就是说程序时静态的代码。
  • 进程时程序的一次执行过程,时系统运行程序的基本单位。系统运行一个程序即是一个进程从创建、运行到消亡的过程。

线程有哪些基本状态

  • NEW:初始状态。线程被构建,但是还没有调用start()方法。
  • RUNNABLE: 运行状态,Java线程将操作系统中的就绪和运行两种状态统称做运行中。
  • BLOCKED:阻塞状态,表示线程阻塞于锁。
  • WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
  • TIME_WAITING:超时等待状态,该状态不同于WAITING,他是可以在指定的时间自行返回的
  • TERMINATED:终止状态,表示当前线程已经执行完毕。

关于final关键字的一些总结

  • final关键字主要用于三个地方:变量、方法、类
  • 对于一个final变量,如果是基本数类型的变量,则其值一旦在初始化之后便不能更改,如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
  • 使用final修饰一个类时,这个类不能被继承,final类中的所有成员方法都会被隐式的指定为final方法。
  • 使用final方法的原因:一是把方法锁定,以防止任何继承类修改它的含义;二是效率,在早期的Java版本中,会将final方法转为内嵌调用,现在的Java版本已经不需要使用final方法进行这些优化了,类中所有的private方法都隐式的指定为final。

Java异常类层次结构图

在这里插入图片描述
在这里插入图片描述

所有的异常都有一个共同的祖先 java.lang.Throwable 类。Throwable 类有两个重要的子类:Exception 异常和 Error 错误。Exception 能够被程序本身处理(try-catch),Error 是无法处理的。

  • Exception: 程序本身可以处理的异常,可以通过catch来进行捕获。Exception又可以分为【受检查异常Check Exception】和【不受检查异常Uncheck Exception】。受检查异常在编码时必须处理,不受检查异常在编码时可以不处理。
  • Error: Error属于程序无法处理的错误,没法通过catch进行捕获,例如Java虚拟机运行错误Virtual MachineError、虚拟机内存不够错误OutOfMemoryError、类定义错误NoClassDefFoundError。这些错误发生时,Jvm一般会选择线程终止。
  • 受检查异常:在代码编译过程中,如果受检查异常没有被catch / throw处理的话,就没办法通过编译。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于检查异常。常见的有:IO相关异常、ClassNotFoundException、SQLException…
  • 不受检查异常:代码在编译过程中,我们即使不处理不受检查异常,也可以正常通过编译。RuntimeException及其子类都统称为非受检查异常,例如:NullPointException、NumberFormatException(字符串转数字异常)、ArraryIndexOutOfBoundsException(数组越界)、ClassCastException(类型转换错误)、ArithmeticException(算术错误)等。

Throwable类常用方法

  • public string getMessage():返回异常发生时的简要描述
  • public string toString(): 返回异常发生时的详细信息
  • public string getLocalizedMessage():返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()相同。
  • public void printStackTrace():在控制台打印Throwable对象封装的异常信息。

异常处理的总结

  • try块:用于捕获异常,其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
  • catch块:用于处理try捕获到的异常。
  • finally块:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前执行。

finally块不会被执行的情况:

  • 在try或finally中使用System.exit(int)退出程序,但是,如果System.exit(int)在异常语句之后,finally还是会被执行。
  • 程序所在的线程死亡。
  • 关闭CPU

当try语句和finally语句中都有return语句时,在方法返回之前,finally语句的内容会被执行,并且finally语句的返回值将会覆盖原始的返回值:
执行f(2),将返回0
在这里插入图片描述

Java序列化中如果有些字段不想进行序列化,怎么办?

对于不想进行序列化的变量,使用transient关键字修饰。
transient关键字的作用是:阻止实例中那些用此关键字修饰的变量序列化;当对象被反序列化时,被transient修饰的变量不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。

Java中的IO流分为几种?

  • 按照流的流向分,可以分为【输入流】和【输出流】
  • 按照操作单元分,可以分为【字节流】和【字符流】
  • 按照流的角色分,可以分为【节点流】和【处理流】

Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上有很对规则,而而且彼此之间存在非常紧密的联系,Java IO流的40多个类都是从如下4个抽象类基类中派生出来的:

  • InputStream/Reader:所有输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。

既然有了字节流,为什么还要有字符流?

字符流是Java虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时的,如果我们不知道编码类型就很容易出现乱码问题。所以,I/O流就干脆提供一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的化,使用字符流比较好。

BIO,NIO,AIO有什么区别?

  • BIO(Blocking I/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数小的情况下,这种模型是不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万以上的连接时,传统的BIO模型是无能为力的,因此,我们需要一种更高效的I/O处理模型来应对更高并发量。
  • NIO(Non-blocking/New I/O):NIO是一种同步非阻塞的I/O模型,在java1.4中引入了NIO框架,对应java.nio包,提供Channel、Selector、Buffer等抽象。它支持面向缓冲的,基于通道的I/O操作方法。对于高负载、高并发的应用,应使用NIO的非阻塞模式来开发。
  • AIO(Asynchronous I/O):AIO也就是NIO2,在java7中引入NIO的改进版NIO2,它是异步非阻塞IO模型。异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会阻塞在那里,当后台处理完成时,操作系统会通知相应的线程进行后续的操作。AIO是异步IO的缩写,虽然在NIO的网络操作中,提供非阻塞的方法,但是NIO的IO行为还是同步的。就目前看来,AIO的应用还不是很广泛,Netty之前也尝试过使用AIO,不过后来放弃了。

深拷贝vs浅拷贝

  • 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递的拷贝。
  • 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容。

List、Set、Map三者的区别?

  • List:存储的元素是有序的,可重复的
  • Set:存储的元素是无序的,不可重复的
  • Map:使用键值对(key-value)存储,key是无序的、不可重复的,value是无序的、可重复的,每个键最多映射到一个值。

ArrayList与LinkedList区别?

  • 是否保证线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全。
  • 底层数据结构:ArrayList底层使用【Object数组】,LinkedList底层使用【双向列表】
  • 插入和删除是否受元素位置的影响:ArrayList采用数组存储,所以插入和删除元素的事件复杂度受元素位置的影响。LinkedList采用链表存储,插入和删除操作时,add()操作时间复杂度不受元素位置的影响,近似O(1);指定位置插入时间复杂度近似为O(n),因为要先移动到指定位置再插入。
  • 是否支持快速随机访问:快速随机访问就是通过元素的序号,快速获取元素对象,即get(int index)。LinkedList不支持高效的随机元素访问,而ArrayList支持。
  • 内存空间占用:ArrayList的空间浪费主要体现在list列表结尾会预留一定的容量空间;而LinkedList的空间花费则体现在它的每一个元素都需要消耗更多的空间,因为要存放直接后继、直接前驱、以及数据。

双向链表和双向循环链表

  • 双向链表,包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。
    在这里插入图片描述
  • 双向循环列表:最后一个节点的next指向head,而head的prev指向最后一个节点,构成一个环。
    在这里插入图片描述
    双向链表:https://juejin.cn/post/6844903648154271757

RandomAccess接口

public interface RandomAccess接口中什么都没有定义,这个接口只是一个标识符罢了,它标识实现这个接口的类具有随机访问功能。

ArrayList实现了RandomAccess接口,而LinkedList没有实现。ArrayList底层是数组,而LinkedList底层是链表,数组天然支持随机访问,时间复杂度为O(1),所以称为快速随机访问。链表需要遍历到特定的位置才能访问特定位置的元素,时间复杂度为O(n),所以不支持快速随机访问。ArrayList实现了RandomAccess接口,标识它具有快速随机访问功能。

RandomAccess接口只是标识,并不是说ArrayList实现了这个接口才具有快速随机访问功能的。

ArrayList与Vector区别?

  • ArrayList是List的主要实现类,底层使用Object[]存储,适用于频繁的查找工作,线程不安全。
  • Vector是List的古老实现类,底层使用Object[]存储,线程安全。

ArrayList的扩容机制

链接

HashMap和Hashtable的区别

  • 线程安全:HashMap是非线程安全的,HashTable是线程安全的,因为HashTable内部基本都经过synchronized修饰。如果要保证线程安全的话,就使用ConcurrentHashMap。
  • 效率:因为线程安全的问题,HashMap的效率要高一点。Hashtable基本被淘汰掉了。
  • 对null Key和Null value的支持:HashMap可以存储null的key和value,但null作为键只能有一个,null作为值可以有多个。HashTable不允许有null键和null值,否则会抛出NullPointException。
  • 初始容量大小和每次扩充容量大小:1.创建时如果不指定容量初始值,Hashtable默认初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始大小为16,之后每次扩充,容量变为原来的2倍。2创建时指定了容量初始值,Hashtable会直接使用给定的大小,而HashMap会将其扩充为2的幂次方大小。也就是说HashMap总会使用2的幂次方作为哈希表的大小。
  • 底层数据结构:JDK1.8以后的HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认8)时,将链表转化为红黑树,以减少搜索时间。Hashtable没有这样的机制。

HashMap和HashSet区别

HashSet底层就是基于HashMap实现的。

  • HashMap实现了Map接口,HashSet实现了Set接口。
  • HashMap存储键值对,HashSet仅存储对象
  • HashMap调用put()想Map中添加元素,HashSet()调用add()向Set中添加元素
  • HashMap使用key计算hashcode,HashSet使用成员对象来计算hashcode

HashSet如何检查重复

当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会认为对象没有重复出现。如果发现有相同的hashcode值对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同,如果两者相同,HashSet就不会让其加入成功。

hashCode()与equals()的相关规定:

  • 如果两个对象相等,则hashcode一定是相等的。
  • 两个相等的对象,equals()返回true
  • 两个对象有相等的hashcoode值,他们也不一定是相等的。
  • 所以,equals()方法被覆盖过,则hashCode()方法也必须被覆盖。
  • hashCode()的默认实现是对堆上的对象产生独特值,如果没有重写hashCode(),则该Class的两个对象无论如何都不会相等,即使这两个对象指向相同的数据。

HashMap的底层实现

JDK1.8之前

  • JDK1.8之前,HashMap底层是【数组+链表】结合在一起使用。HashMap通过key的hashCode经过扰动函数处理过后得到的hash值,然后通过(n-1)& hash判断当前元素存放的位置(n为数组长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同的话,就直接覆盖,如果不同的话,就通过【拉链法】解决冲突。
  • 拉链法:将链表与数组相结合,创建一个链表数组,数组中的每一个元素就是一个链表,若遇到哈希冲突,则将冲突的值加到链表中即可。
    JDK1.8之后
  • JDK1.8之后,在解决哈希冲突有了较大的变化,当链表长度大于阈值(默认8),将链表转化为红黑树,以减少搜索时间。将链表转化为红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换红黑树。
  • TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

ConcurrentHashMap和Hashtable的区别?

ConcurrentHashMap和Hashtable的区别主要体现在实现线程安全的方式上不同。

  • 底层数据结构:JDK1.7的ConcurrentHashMap底层采用【分段数组+链表】实现,JDK1.8采用数据结构跟HashMap1.8的结构一样,【数组+链表/红黑树】的形式。数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
  • 实现线程安全的方式:1.JDK1.7,ConcurrentHashMap对整个桶数组进行了分割分段,每把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提供并发效率。JDK1.8,抛弃了分段的概念,直接用Node数组+链表+红黑树来实现,并发控制使用synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。2. Hashtable使用synchronized来保证线程安全,全表锁,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如果使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率低下。

比较HashSet、LinkedHashSet、TreeSet三者的异同。

  • HashSet是Set接口的主要实现类,HashSet的底层是HashMap,线程不安全的,可以存储null值。
  • LinkedHashSet是HashSet的子类,能够按照添加的顺序遍历。
  • TreeSet底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。

集合框架底层数据结构总结

Collection接口下面的集合:

  • List: Arraylist(Object[] 数组)、Vector(Object[] 数组)、LinkedList(双向链表)
  • Set: HashSet(无序、唯一,基于HashMap实现,底层采用HashMap来保存元素)、LinkedHashSet(LinkedHashSet是HashSet的子类,并且其内部是通过LinkedHashMap实现的)、TreeSet(有序,唯一,红黑树实现)

Map接口下面的集合:

  • HashMap、LinkedHashMap、Hashtable、TreeMap

如何选用集合

  • 需要根据键值获取元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时选择HashMap,需要保持线程安全时选用ConcurrentHashMap。
  • 当只需要存放元素值时就选用Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合,比如TreeSet或者HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList。

什么时线程和进程

什么是进程?

  • 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进行时动态的。系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • Java中,当我们启动main函数时就是启动了一个JVM进程,而main函数所在的线程就是这个进程中的一个线程,也称做主线程。
    什么是线程?
  • 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。

简要描述线程与进程的关系、区别、优缺点

  • 一个进程可以有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器、虚拟机栈、本地方法栈。
  • 线程时进程划分成更小的运行单位。
  • 线程和进程最大的不同在于基本上各个进程时独立的,而各线程则不一定,因为同一进程中的线程极可能会互相影响。
  • 线程执行开销小,但是不利于资源的管理和保护,而进程正相反。

程序计数器为什么是私有的?
程序计数器主要有两个作用:

  • 字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制,如顺序执行、选择、循环、异常处理。
  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,当线程被切换回来时,能够知道线程上次执行到那里了。

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

虚拟机栈和本地方法栈为什么是私有的?

  • 虚拟机栈:每个Java方法再执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在Java虚拟机栈中入栈和出栈的过程。
  • 本地方法栈:和虚拟机栈所发挥的所用非常相似,区别是虚拟机栈为虚拟机运行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。在HotSpot虚拟机中和Java虚拟机栈合二为一。

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是私有的。

简述堆和方法区

堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象(所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

并发与并行的区别

  • 并发:同一时间段,多个任务都在执行(单位时间内不一定同时执行)。
  • 并行:单位时间内,多个任务同时执行。

为什么要使用多线程

  • 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核CPU时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。

使用多线程可能带来什么问题?

并发编程的目的就是为了能提高程序的执行效率,提高运行速度,当时并发变成并不是总能提搞程序运行速度的,而且并发变成可能遇到很多问题,比如:内存泄露、上下文切换、死锁。

什么是上下文切换?

当一个线程的时间片用完的时候就会处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
Linux相比其他操作系统有很多优点,其中一项就是,其上下文切换和模式切换的时间消耗非常少。

什么是线程死锁?

多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放,由于线程被无限期的阻塞,因此程序不可能正常终止。
产生死锁必须具备以下四个条件:

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对以获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺只有自己使用完毕后才释放资源。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免线程死锁

为了避免死锁,我们只要破坏产生死锁的四个条件之一即可。

  • 破坏互斥性:这个条件无法破坏,因为用所本来就是想让他们互斥的。
  • 破坏请求与保持条件:一次性申请所有资源。
  • 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件:靠按序申请资源来预防,按某一顺序申请资源,释放资源则反序释放,破坏循环等待条件。

多线程为什么调用start()方法时会执行run()方法,为什么不直接调用run()方法

new一个Thread,线程进入新建状态,调用start()方法,会启动一个线程并使线程进入就绪状态,当分配到时间片之后就可以开始运行了。start()会执行线程的相应准备工作,然后自动自行run()方法的内容,这是真正的多线程工作。但是直接执行run()方法,会把run()方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
调用start()方法方可启动线程并使线程进入就绪状态,直接执行run()方法的话不会以多线程的方式执行。

说一说自己对synchronized关键字的了解

synchronized关键字解决的是多个线程之间资源的同步性,synchronized关键字可以保证它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized关键字的主要三种使用方式:

  • 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。
  • 修饰静态方法:也就是给当前类加锁,会作用于类的所有的对象实例,进入同步代码前要获得当前class的锁。因为静态成员不属于任何一个实例对象,是类成员。如果一个线程调用一个实例对象的非静态synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象,因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized方法占用的是当前实例对象锁。
  • 修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this或object)表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)表示进入同步代码前要获得当前class的锁。

synchronized关键字加到static静态方法和synchronized(class)代码块上都是给Class类上锁。synchronized关键字加到实例方法上是给对象实例上锁。

构造方法可以使用synchronized关键字修饰么

构造方法不能使用synchronized关键字修饰。构造方法本身就属于线程安全的,不存在构造方法一说。

synchronized关键字和volatile关键字的区别

synchronized关键字和volatile关键字是两个互补的存在,而不是对立的存在!

  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法及代码块。
  • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。

发表评论