这里只会写Java相关的问题,包括Java基础问题、JVM问题、线程问题等。全文所使用图片,部分是自己画的,部分是自己百度的。如果发现雷同图片,联系作者,侵权立删。
- 1. Java基础面试问题
- 1.1 基本概念相关问题
- 1.1.1 Java语言有什么特点?
- 1.1.2 java 是什么类型的语言?
- 1.1.3 面向对象是什么意思?都有什么特性?
- 1.1.4 介绍一下Java方法重载?
- 1.1.5 抽象类和接口有什么区别?
- 1.1.6 什么时候用抽象类,什么时候用接口?
- 1.2 数据变量相关的问题
- 1.2.1 Java基础数据变量有哪些?
- 1.2.2 每个数据变量类型占几个字节?有多少位?
- 1.2.3 了解包装类型吗?解释下
- 1.2.4 解释下装箱(Boxing)和拆箱(Unboxing)
- 1.2.5 i++ 和 ++i 有什么区别?
- 1.2.6 浮点数精度丢失问题了解吗?怎么解决?
- 1.2.7 `transient` 修饰的成员变量,有什么特点?
- 1.3 表述下你对Java变量的了解?
- 1.4 什么是形参?什么是实参?有什么区别?
- 1.5 注解相关问题
- 1.5.1 Java都有哪些元注解?每个元注解分别有什么含义?
- 1.5.2 注解可以被继承吗?
- 1.6 synchronized 和 volatile 的区别?
- 1.7 指针拷贝、浅拷贝和深拷贝解释下?
- 1.8 Java Object相关问题?
- 1.8.1 == 和 Object::equals 区别?
- 1.8.2 `Object::hashCode` 方法是什么?有什么用?
- 1.8.3 为什么重写`Object::equal`方法,建议要重写`Object::hashCode`方法?
- 1.8.4 `Object::sleep`和`Object::wait`方法清楚吗?有什么区别?
- 1.9 java 字符串相关问题
- 1.9.1 String,StringBuilder,StringBuffer之间有什么区别?
- 1.9.2 String为什么长度是不可变的?
- 1.9.3 StringBuilder为什么长度可变?
- 1.9.4 StringBuffer为什么是线程安全的?
- 1.9.5 什么时候用StringBuffer,什么时候用StringBuilder?
- 2 Java集合相关问题
- 2.1 介绍一下JAVA集合?
- 2.2 Set集合问题
- 2.2.1 Set集合怎么保证元素不可重复?
- 2.2.3 Set 常见的实现类有哪些?
- 2.2.4 在HashSet中放入一个元素,Set集合是怎么工作的?
- 2.2.5 TreeSet 怎么保证元素有序?
- 2.3 List集合问题
- 2.3.1 常见的List实现类有哪些?
- 2.3.2 Array、ArrayLIst和LinkedLIst区别?
- 2.3.3 ArrayLIst可以添加Null元素?LinkedLIst可以添加Null元素吗?
- 2.3.4 了解Vector吗?介绍下
- 2.3.5 Vector为什么不被推荐使用?
- 2.3.6 CopyOnWriteArrayList了解吗?介绍下?
- 2.3.7 CopyOnWriteArrayList 怎么保证线程安全性?
- 2.3.8 `CopyOnWriteArrayList` 为什么比`Vector`更快,更推荐使用?
- 2.4 Map集合问题【重点】
- 2.4.1 简单介绍下Map集合?
- 2.4.2 常见的Map集合有哪些?
- 2.4.3 HashMap 和 HashTable的区别是什么?
- 2.4.4 HashMap底层是怎么存储数据的?
- 2.4.5 put一个元素时,HashMap是怎么处理的?
- 2.4.6 HashMap为什么初始大小是16?有什么说法?【HashMap初始大小为什么是2幂次方】
- 2.4.7 HashMap 和 ConcurrentHashMap 的区别是什么?
- 2.4.8 了解 ConcurrentHashMap 吗?简单介绍下?
- 2.4.7 ConcurrentHashMap底层数据结构介绍下?
- 2.4.8 冷门问题:ConcurrentHashMap为什么要升级结构?
- 2.4.9 ConcurrentHashMap怎么保证线程安全性的?
- 2.4.10 ConcurrentHashMap 和 HashTable 的区别?
- 2.5 queue相关问题
- 2.5.1 请解释一下Queue是什么?
- 2.5.2 Java中Queue接口有哪些实现类?
- 2.5.3 哪些Queue实现类是线程安全的?
- 2.5.4 如何在使用非线程安全的Queue时保证其线程安全?
- 2.5.5 请简述一下Queue在消息队列(Message Queue, MQ)中的应用?
- 2.5.6 Queue的容量限制是如何处理的?
- 2.5.7 Queue在并发环境下的性能表现如何?
1. Java基础面试问题
1.1 基本概念相关问题
1.1.1 Java语言有什么特点?
很多人都能想到“once written, anywhere run”这句话,但是其他的基本上都回答不出来了。
- 平台无关性,一次编写,多平台调用,这个特性主要是取决于Java是半解释半编译语言
- 自动内存管理机制,比如有垃圾回收机制,内存分配机制等等
- 面向对象(不是继承啊,面向对象包含继承)
- 动态加载,也可以叫动态平衡,例如
泛型
就是典型动态平衡过程。 - 简单易上手,Java属于高级语言,其编程比较贴合现实语言,容易理解。
1.1.2 java 是什么类型的语言?
Java严格来说是半解释半编译的语言,解释性语言表示将代码文件解释为机器可执行的指令,然后再去执行。而编译性的语言,会将代码直接转换为机器可执行的指令。两者的区别在于,前者解释执行属于一个过程,后者则是拿到结果集去直接执行。
但对于Java而言,首先要把.java
文件编译为.class
文件,然后再通过解析器解释.class
文件去执行。
1.1.3 面向对象是什么意思?都有什么特性?
面向对象实际是一种思想,简而言之就是将事务拆分成多个整体去看,具体的业务也是整体之间的关系而已。
举个简单例子,例如:你要去买电脑。电脑和你就是两个独立的整体,买是一个行为,是你和电脑整体之间的联系。至于电脑的品牌,则是一个对象的实例。
尽量举例子回答,可以让面试官直观的能感受到你是理解了,而不是背下了。
Java的面相对象具备三个特性,分别是:
- 继承:对象与对象之间是可以继承的,Java将对象定义为一个类,继承该类的对象叫子类,被继承的叫父类。子类具备父类对外能力,子类实际是父类能力的一次扩充。Java只支持单继承。
- 封装:除了对象暴露出来的能力,其他信息对外是不可见的
- 多态:对象允许同一个接口,通过不同实现,来处理不同的逻辑。多态分为编译时多态和运行时多态,编译时多态主要通过方法的重载(Overload)来实现,即同一个类中有多个同名方法,但它们的参数类型或参数个数不同。运行时多态则主要通过方法的覆盖(Override)和向上转型(父类引用指向子类对象)来实现。通过多态,我们可以编写出更加灵活和可扩展的代码,以适应不断变化的需求。
1.1.4 介绍一下Java方法重载?
简单的来说,Java重载就是同一个对象中,方法同名不同用。具体的就是,它允许在同一个类中定义多个同名的方法,只要它们的参数列表不同即可。这个参数列表不仅指参数类型,也包括参数个数。
1.1.5 抽象类和接口有什么区别?
- 从接口定义而言,抽象类是通过
abstract
定义的,接口是通过interface
定义的。 - 从包含内容而言,抽象类可以包含正常类的一切内容,例如方法、变量等。接口只能包含静态常量、静态方法、默认方法、方法定义。
- 从继承特性而言,一个类的直接继承关系里面,只能继承一个抽象类,但可以继承(或者叫实现)多个接口。
- 从实例化而言,抽象类可以包含构造方法,但不能被实例化;接口不能包含构造方法,也不能实例化。
- 从一般使用而言,抽象类更像是把一些通用的逻辑,放到一个类里面去实现,子类可以直接调用抽象类的实现,从而避免子类冗余代码。接口更像是一组对外的功能定义,定义具体的方法,其他类可以直接通过接口感知到类实例提供的能力。
1.1.6 什么时候用抽象类,什么时候用接口?
根据抽象类和接口的定义和内容不同,如果只是单纯的想要一些抽象方法,而不需要额外的处理逻辑,可以考虑使用接口。反过来,使用抽象类。
1.2 数据变量相关的问题
1.2.1 Java基础数据变量有哪些?
有8种,分别是:float、double、byte、short、int、long、char和boolean。
1.2.2 每个数据变量类型占几个字节?有多少位?
float:4个字节,32位
double:8个字节,64位
byte:1个字节,8位
short:2个字节,16位
int:4个字节,32位
long:8个字节,64位
char:2个字节,16位
boolean:没有具体规定
1.2.3 了解包装类型吗?解释下
包装类型属于基础类型的一个扩充,让基础类型具备对象特性。
Java 中的包装类型(Wrapper Classes)是 Java 提供的一种特殊的类,用于将基本数据类型封装成对象。这样做有几个主要的原因:
-
提供对象化的基本数据类型:基本数据类型(如 int, double, boolean 等)不是对象,因此它们没有属性和方法。包装类型允许你将基本数据类型当作对象来处理,这样就可以使用对象所拥有的功能,比如作为集合(如 List, Set)的元素,或者在使用泛型时作为类型参数。
-
提供额外的方法:包装类型提供了许多有用的方法,比如类型转换(
Integer.parseInt(String s)
)、比较(Integer.compare(int x, int y)
)、缓存(对于 Integer, Byte, Short, Character, Long, Boolean 来说,当值在特定范围内时,会使用缓存的实例)等。 -
满足泛型的需求:Java 泛型的设计初衷是支持对象类型,而不支持基本数据类型。因此,如果你需要使用泛型,并且想在其中包含基本数据类型,就必须使用对应的包装类型。
Java 中的基本数据类型和它们对应的包装类型如下:
- 基本数据类型:
byte
,short
,int
,long
,float
,double
,char
,boolean
- 包装类型:
Byte
,Short
,Integer
,Long
,Float
,Double
,Character
,Boolean
1.2.4 解释下装箱(Boxing)和拆箱(Unboxing)
- 装箱:将基本数据类型转换为对应的包装类型的过程。例如,
int
转换为Integer
。Java 自动进行装箱操作,这个过程称为自动装箱(Autoboxing)。 - 拆箱:将包装类型转换为对应的基本数据类型的过程。例如,
Integer
转换为int
。Java 自动进行拆箱操作,这个过程称为自动拆箱(Autounboxing)。
1.2.5 i++ 和 ++i 有什么区别?
- ++i(前缀自增):这种自增方式称为“前缀自增”,因为它发生在表达式求值之前。
- i++(后缀自增):这种自增方式称为“后缀自增”,因为它发生在表达式求值之后。
1.2.6 浮点数精度丢失问题了解吗?怎么解决?
浮点数精度丢失问题是计算机编程中常见的一个问题,主要是由于计算机内部使用二进制来表示浮点数,而某些十进制小数在二进制下无法精确表示,从而导致了精度丢失。例如0.1在二进制下就是无限循环小数,由于float和double长度有限,所以会丢失部分数据。解决办法也很简单,使用BigDecimal
来代替浮点数计算,因为BigDecimal
内部使用了字符串代替了浮点数。
1.2.7 transient
修饰的成员变量,有什么特点?
当一个类的成员变量被transient修饰后,这个变量在类的序列化过程中将被忽略,即该变量不会被序列化到输出流中,也不会从输入流中反序列化回来。
1.3 表述下你对Java变量的了解?
Java变量是程序中用于存储数据值的一个容器。在Java中,每个变量都有一个类型,这个类型决定了变量可以存储哪种类型的数据,以及变量可以进行的操作。变量还拥有一个名称(标识符),通过这个名称,我们可以在程序中引用或操作变量的值。
变量的基本要素
-
变量名(标识符):变量的名称,用于在程序中引用变量。变量名必须是合法的标识符,它必须以字母、下划线(_)、美元符号($)开头,后面可以跟字母、数字、下划线或美元符号。变量名不能与Java的保留关键字相同。
-
变量类型:变量的类型决定了变量可以存储的数据类型。Java是一种强类型语言,这意味着每个变量都必须声明一个类型。Java的基本数据类型包括整型(int、byte、short、long)、浮点型(float、double)、字符型(char)、布尔型(boolean)等。除了基本数据类型外,Java还支持对象类型(如String、自定义类等)。
-
变量值:存储在变量中的具体数据。变量的值可以在程序执行过程中被改变(对于可变类型的变量)。
变量的声明和初始化
在Java中,声明变量的一般语法是:
type variableName;
其中,type
是变量的类型,variableName
是变量的名称。
声明变量后,可以在声明时或之后的某个时刻给变量赋值。赋值的一般语法是:
variableName = value;
其中,value
是要赋给变量的值,它的类型必须与变量的类型兼容。
示例
int age; // 声明一个整型变量age
age = 25; // 给变量age赋值String name = "John Doe"; // 声明并初始化一个字符串变量namedouble pi = 3.14; // 声明并初始化一个双精度浮点型变量pi
注意事项
- 变量名在Java中是区分大小写的。
- 变量在声明之后、使用之前必须被初始化(除了局部变量,它们可以在声明时初始化,或者在首次使用之前被初始化)。
- 变量的作用域决定了变量在程序中的可见性和生命周期。
1.4 什么是形参?什么是实参?有什么区别?
形参和实参都指方法调用时,传入的参数,只不过所表达的意思不同而已。
- 形参:Java是通过句柄池来管理对象内存信息的,所谓的形参就是指句柄池的指针,传递时本身没有具体的值。
- 实参:和形参不同,实参直接传递的是已经定义好的值,例如用基础数据类型做变量时,就是实参。
- 区别在于:
- 形参是指针,本身没有值;实参是具体的值,已经被定义好。
- 形参的改变,不会影响指针指向的对象,但会影响对象内的值;实参的改变,并不会影响外面的传入值。【实参的传递,更像拷贝了一份值,直接传递给方法使用,方法内部改变值并不会影响外边的数据】
1.5 注解相关问题
1.5.1 Java都有哪些元注解?每个元注解分别有什么含义?
Java中的元注解是用于定义注解的注解,它们提供了对注解进行进一步描述的机制。Java标准API定义了以下几种元注解:
-
@Retention
- 作用:用于指定被它注解的注解保留的时间。
- 取值:
RetentionPolicy.SOURCE
:注解仅在源代码中保留,编译后不会保留。RetentionPolicy.CLASS
:注解在编译时被保留,但在运行时不会被加载到JVM中。RetentionPolicy.RUNTIME
:注解在运行时被保留,并且可以通过反射机制读取。
-
@Target
- 作用:用于指定被它注解的注解可以应用的地方。
- 取值:ElementType枚举类中的值,包括但不限于
TYPE
(类、接口、枚举)、FIELD
(字段)、METHOD
(方法)、PARAMETER
(参数)等。
-
@Documented
- 作用:用于指定被它注解的注解是否会包含在JavaDoc文档中。
- 说明:如果一个注解被
@Documented
注解,那么该注解在生成Javadoc时会被包含在生成的文档中。
-
@Inherited
- 作用:用于指定被它注解的注解是否可以被子类继承。
- 说明:如果一个被
@Inherited
注解的注解应用在一个类上,并且这个类的子类没有应用任何注解,则子类会继承父类的注解。但需要注意的是,被注解的接口的子类不会继承该注解。
-
@Repeatable(Java 8引入)
- 作用:用于指定同一个位置该注解是否能被重复使用。
- 说明:在Java 8之前,同一个地方不能重复使用同一个注解。Java 8引入
@Repeatable
注解后,可以通过定义一个容器注解来实现在同一个地方重复使用某个注解。
1.5.2 注解可以被继承吗?
从Java语言定义来讲,一个@annotation定义的注解,不能extends
来继承另一个@annotation定义的注解。但是可以通过一些具体的操作,来达到实现继承的目的,例如:
- @Inherited元注解:当一个注解被@Inherited元注解修饰时,它表示该注解具有继承性。即,如果一个类使用了这个注解,那么它的子类(没有显式使用相同注解的情况下)也会被视为使用了这个注解。但需要注意的是,这种继承只适用于类上的注解,不适用于方法、字段等其他元素上的注解。此外,即使使用了@Inherited,注解的继承性也是有限的,它并不等同于类之间的继承关系。
- 组合注解:虽然注解本身不支持继承,但可以通过定义新的注解并包含其他注解作为元素(即元注解中的@Retention、@Target等)的方式来实现类似继承的效果。这种方式实际上是通过组合多个注解来模拟继承的行为。
1.6 synchronized 和 volatile 的区别?
synchronized
和volatile
都是Java
中用于处理多线程同步的机制,但它们之间存在一些关键的区别。
- 作用位置和使用方式:
synchronized
可以作用到方法、代码块、变量和类,而volatile只能作用到变量。 - 功能与特性:
synchronized
通过加锁,可以保证数据的可见性、程序原子性、线程阻塞、资源互斥。volatile
只能保证数据的可见性。 - 内存消耗和性能:由于
synchronized
有锁的相关操作,所以存在一定的内存开销。由于多线程的阻塞和唤醒操作,所以对性能会产生一定影戏那个。而volatile
只是对线程缓存的屏蔽,来保证数据的可见性。本身不会存在锁机制,所以在性能和资源开销方面,小于Synchronized
。
1.7 指针拷贝、浅拷贝和深拷贝解释下?
Java的对象是放在堆内存中的,然后通过句柄池中的指针来管理的。
- 指针拷贝:指堆内存中的对象地址不变,而复制句柄池的指针,让新指针和旧指针同时指向内存对象。
- 浅拷贝:会复制堆内存中的对象,创建一个新的对象。如果对象内 存在指针 指向别的对象,则只会复制指针,并不会复制指针指向的对象信息。
- 深拷贝:完全复制一个完整的对象信息,包括对象内指针指向的对象信息。
1.8 Java Object相关问题?
1.8.1 == 和 Object::equals 区别?
==
是直接比较对象值,例如 A == B
,如果A和B都是基本数据类型,则比较A和B的值;如果A和B都是对象,则比较A和B的指针地址指。
Object::equals
则是对象的一个方法,只能做对象之间的比较。而且Object::equals
是可以被继承重写的,例如String::equals
重写覆盖了Object::equals
方法。如果不重载,则默认和==
是一样的。
1.8.2 Object::hashCode
方法是什么?有什么用?
Object::hashCode
主要用来获取一个对象的哈希值,本身底层是调用C++的一个本地方法计算对象的地址来获取哈希值。Hash code主要是用来获取一个对象的哈希值,哈希值经常被用作做唯一判断处理。例如在hashmap
中对于key的唯一性就是用哈希值来做的,而且在一些object::equals
中,也存在利用哈希值判断对象是否相等。
1.8.3 为什么重写Object::equal
方法,建议要重写Object::hashCode
方法?
Object::equal
方法是用来判断两个对象是否相等,Object::hashCode
是用来获取一个对象的哈希值,如果不重写Object::hashCode
,可能会引发一些唯一性的判断异常。比如说在hashMap
中,hashMap
会利用对象的hash code值来判断一个键值对的一个位置,如果两个对象的equals方法是相等的,但是hashCode
值是不相等的。那么在一个put
操作过程中,可能就会导致两个相等的对象,放到不同的位置,而且丢失key的唯一性。
1.8.4 Object::sleep
和Object::wait
方法清楚吗?有什么区别?
之前被问过,这特么是一个坑,我义无反顾的踩进去了。
Object
没有sleep
方法,Object
只有wait
方法,作用是挂起当前线程,让出当前线程的执行资源。
1.9 java 字符串相关问题
1.9.1 String,StringBuilder,StringBuffer之间有什么区别?
- 长度方面,string是长度不变的,string builder和string buffer是长度可变的。
- 线程安全方面,string和string buffer是线程安全的,StringBuilder是线程不安全的。
1.9.2 String为什么长度是不可变的?
String底层是用byte数组去存储它的值的,而这个数组的长度是固定的。如果发生数值或者说长度改变,就相当于创建一个新的字符串。如果发生数值或者说长度改变,就相当于创建一个新的字符串。
但是在JAVA 17及以上版本,string的长度是可变的。
1.9.3 StringBuilder为什么长度可变?
String builder的字符串存储结构是数组加链表,每个字符都放在数组中,数组之间用链表指针关联。一个新的字符放到string builder中,会在数组的末尾新增一个字符。如果这个新增的字符加上数组本身的长度超过数组的长度,则会新建一个数组,然后通过指针把这个数组关联到上一个数组的末尾,最后将这个新增的字符插入新的数组中。
1.9.4 StringBuffer为什么是线程安全的?
StringBuffer底层会对一些关键性的操作通过synchronized关键字进行加锁,多个线程在调用或者修改StringBuffer时,由于锁的关系会进行阻塞。其他线程只能等当前线程对StringBuffer操作完成之后才能进行操作。
1.9.5 什么时候用StringBuffer,什么时候用StringBuilder?
仅就线程安全性考虑,如果需要可变字符串提供线程安全能力,那用StringBuffer,如果不需要则用StringBuilder。在同一个线程内需要用到可变字符串时,如果不存在共享变量的情况,则用StringBuilder比StringBuffer好,可以提升效率的10%~15%。
2 Java集合相关问题
先了解一下Java集合的继承关系
这个心中要有一个基本的概念,不然问的时候,驴唇不对马嘴,就尴尬了。
2.1 介绍一下JAVA集合?
JAVA集合分为两大类,一类是继承collection接口的集合,另外一类是继承map接口的集合。
这两类集合的区别在于collection接口的集合是对单值进行收集管理,而map集合则是对KV键值对进行收集管理。
collection接口的继承类包括set,List,Queue, Set集合元素不可重复;List是一个列表,支持随机访问和获取;Queue是队列,支持FIFO等数据结构特性。
Map接口的继承类包括AbstractMap和SortedMap,常见的实现类有HashMap 和 ConcurrentHashMap,TreeMap 等。
2.2 Set集合问题
2.2.1 Set集合怎么保证元素不可重复?
Set集合在新增元素的时候会将元素和集合内的元素进行比较,如果两个对象相等,则拒绝插入,如果不相等,则插入,比较是通过对象的equal()
方法进行的。
2.2.3 Set 常见的实现类有哪些?
Set 常见的实现类有 HashSet和TreeSet
2.2.4 在HashSet中放入一个元素,Set集合是怎么工作的?
HashSet 底层维护了一个HashMap,当放入元素的时候,会调用HashMap的put方法,这个元素作为key进行唯一性判断。
2.2.5 TreeSet 怎么保证元素有序?
在新建TreeSet的时候,需要提供一个Comparator比较器,用来比较元素顺序。如果不提供,则会调用元素本身的比较方法。如果元素没有提供比较方法,则比较两个元素的哈希值。
在TreeSet中,底层维护了一个TreeMap,而TreeMap存储key的是一个红黑树结构。当放入一个元素的时候,相当于将这个元素放入一个红黑树中。红黑树本身就是一个有序结构树,所以保证了集合元素的有序性。
2.3 List集合问题
2.3.1 常见的List实现类有哪些?
List常见的实现类有 LinkedLIst 和 ArrayLIst。
2.3.2 Array、ArrayLIst和LinkedLIst区别?
Array是一个数组,具有固定长度,每个数组元素是维护在虚拟栈上面的。而ArrayLIst和LinkedLIst长度可变,其中ArrayLIst底层存储结构是多个连续的Array数组,而LinkedLIst采用的双向循环链表。
由于数据结构的关系,ArrayLIst在随机访问方面,强于LinkedLIst,而在插入和删除元素方面,逊色与LinkedLIst。
2.3.3 ArrayLIst可以添加Null元素?LinkedLIst可以添加Null元素吗?
都可以添加
2.3.4 了解Vector吗?介绍下
Vector是可变集合,继承了AbstractLIst类,而AbstractLIst实现了List接口,所以说Vector具备LIst的所有能力。
而且Vector是线程安全的,Vector对每一个方法都施加了synchronized
锁,保证了线程安全性,但同样降低了其访问效率。一般在JDK工程中,已经不推荐使用了。
2.3.5 Vector为什么不被推荐使用?
- 从使用场景来看,Vector并没有好的使用场景,在多线程变成过程中,很少有用到安全动态数组的场景,如果有用到,完全可以用
ConcurrentHashMap
代替。 - 从性能效率来看,Vector由于锁的关系,存在额外的锁性能和资源消耗。比起其他无所动态列表来说,更慢,比如ArrayLIst。
2.3.6 CopyOnWriteArrayList了解吗?介绍下?
CopyOnWriteArrayList
是一个线程安全的ArrayList
变体,提供了一些列手段,来保证它的线程安全性。CopyOnWriteArrayList
适合读多写少,需要数据保持高度一致性的场景。CopyOnWriteArrayList
是弱迭代一致性,在迭代过程中,迭代修改的信息并不会理解反馈到数组中。
2.3.7 CopyOnWriteArrayList 怎么保证线程安全性?
CopyOnWriteArrayList
通过 CAS + Copy + Volatile 来保证线程安全性,在CopyOnWriteArrayList
底层,维护了一个数组,该数组用volatile
关键字做了修饰,屏蔽了数组初始化的指令重排,保证了需改后再线程之间的可见性。
其次, CopyOnWriteArrayList
在每次操作写操作之前,都会先复制一份数组副本,然后在副本上进行数据写入,最后通过CAS将原本替换为副本。
2.3.8 CopyOnWriteArrayList
为什么比Vector
更快,更推荐使用?
CopyOnWriteArrayList
尽量避免了 synchronized
使用,在读的时候没有加锁,在写的时候则使用CAS保证数据一致性。而Vector则是全文加锁,通过锁来保证数据一致性。两者之间,加锁明显慢。
2.4 Map集合问题【重点】
2.4.1 简单介绍下Map集合?
Map集合存储的是KV键值对,要求K之间不能相等,而且每个KV都可以通过K来获取相应的V值。
2.4.2 常见的Map集合有哪些?
HashMap,ConcurrentHashMap,TreeMap等。
2.4.3 HashMap 和 HashTable的区别是什么?
- hashMap是线程不安全的,hashTable是线程安全的。【hashTable 基本上所有操作都通过
synchronized
加锁处理了】 - hashMap支持放入Null key和Null value,而HashTable不可以,放入就报错。
- HashMap初始容量是16,HashTable是11
2.4.4 HashMap底层是怎么存储数据的?
HashMap
底层在JDK8之前是数组+链表,JDK8及以后都是数组+链表+红黑树。
2.4.5 put一个元素时,HashMap是怎么处理的?
HashMap
的存储结构是数组+链表+红黑树,初始数组默认大小是16。
- 首先会取K的哈希值,然后与数组大小做模运算,确定桶(Bucket)位置。
- 其次会判断该捅位上是否有和插入元素相同的值,如果有,则更新V值;没有则采用拉链法,给桶关联的链表新增一个元素。
- 如果链表的长度新增一个元素后,超过8。在此前提下,如果数组大小小于64,则扩容数组,再对数组取摸运算,插入新数组;否则则将链表转换为红黑树,将元素并入红黑树。
2.4.6 HashMap为什么初始大小是16?有什么说法?【HashMap初始大小为什么是2幂次方】
准确的来说,初始值大小是2幂次方。在HashMap
在插入新元素时,会取K的哈希值和数组取模确定桶的位置。取模操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作,而16等于2的4次方。对于一个很大的哈希值,计算机与(&)操作比取模快。
2.4.7 HashMap 和 ConcurrentHashMap 的区别是什么?
- 线程安全性方面,HashMap是线程不安全的,ConcurrentHashMap 是线程安全的
- KV存储方面,HashMap允许KV为Null,而ConcurrentHashMap不允许
- 存储结构方面:HashMap底层是用
数组+链表+红黑树
存储,ConcurrentHashMap 在JDK8以前是Segment 数组 + HashEntry 数组 + 链表
,JDK8以后则是Node数组+链表+红黑树
2.4.8 了解 ConcurrentHashMap 吗?简单介绍下?
ConcurrentHashMap 是Java提供的一个并发Map集合工具,在java.util.concurrent
包下。主要是用来在多线程编程下,解决资源访问安全性问题。
2.4.7 ConcurrentHashMap底层数据结构介绍下?
JDK7版本及以前,ConcurrentHashMap
的底层存储结构为segment 数组 + HashEntry数组 + 链表
,所有的元素值存储在HashEntry数组 + 链表
中,segment 数组主要用来做加锁处理;JDK8版本及以后,ConcurrentHashMap
的底层存储结构为Node 数组 + 链表 + 红黑树
;
2.4.8 冷门问题:ConcurrentHashMap为什么要升级结构?
- 提高并发性能,在JDK7之前,采用的是分段锁设计来保证线程安全性。而Segment大小在初始化Map对象时就已经确定,是一个固定值。每个线程访问ConcurrentHashMap时,都需要对Segment加锁,换言之就是Segment初始化大小是多少,就支持多少个线程并发。在多核CPU的硬件普及下,需要更高的并发性能,显然Segment这种粗粒度锁不适合现代计算机开发。
- 优化数据结构,在JDK7之前采用
HashEntry数组+链表
存储元素值,如果元素过多,这会导致链表过长,在随后的访问会导致时间复杂度上升,影响性能。随后优化为数组+链表+红黑树,如果链表过长,则会转换为红黑树。在处理大量元素时,红黑树明显比链表性能更优秀。 - 简化设计和实现,采用Segment数组设计比较复杂难以理解,采用Node则更简单了。
2.4.9 ConcurrentHashMap怎么保证线程安全性的?
这里只说JDK8版本,JDK8 是采用 Node数组 + 链表 + 红黑树
来存储元素的,其中Node数组在声明时用了volatile
关键字修饰,保证对于node数组
修改是对其他线程可见的。
在进行元素修改操作时,会先对操作的K取模,确定Node数组位置。如果Node是一个Null值,则初始化该Node,并利用CAS更新Node数组。如果不为Null,则通过synchronized
给Node加锁,防止其他线程修改此Node数据,但不影响其他线程操作其他Node数组元素,然后在锁内修改元素值。
2.4.10 ConcurrentHashMap 和 HashTable 的区别?
ConcurrentHashMap
和 HashTable
都是Java提供的线程安全的集合工具,JDK8区别如下:
- 数据存储结构不同,
ConcurrentHashMap
采用的是Node数组 + 链表 + 红黑树
,HashTable
采用的是数组 + 链表。 - 实现线程安全的方式不用,
ConcurrentHashMap
采用的是在Node数组节点上加锁,来实现线程安全性;而HashTable则在每个方法上加锁,来实现线程安全性。 - 扩容机制不同:
ConcurrentHashMap
扩容是在Node数组基础上扩容,HashTable
则是复制原本数组再扩容。 - 性能方面:
HashTable
的性能不如ConcurrentHashMap
2.5 queue相关问题
关于Queue相关的面试题,可以从多个角度进行考察,以下是一些可能的面试问题及详细解答:
2.5.1 请解释一下Queue是什么?
Queue是一种先进先出(FIFO, First-In-First-Out)的数据结构,它只允许在队列的一端(队尾)进行插入(enqueue)操作,在另一端(队头)进行删除(dequeue)操作。Queue用于存储一系列等待处理的元素,常用于实现任务调度、缓冲、消息传递等场景。
2.5.2 Java中Queue接口有哪些实现类?
Java中Queue接口有多个实现类,包括但不限于:
- LinkedList:基于链表实现的队列,非线程安全。
- PriorityQueue:基于优先级堆的无界优先级队列,非线程安全,元素按照其自然顺序或构造队列时所提供的Comparator进行排序。
- ArrayDeque:基于动态数组的双端队列,非线程安全,但可以作为高效的队列使用。
- LinkedBlockingQueue:基于链表结构的阻塞队列,线程安全,支持多线程并发访问。
- ArrayBlockingQueue:基于数组结构的阻塞队列,线程安全,同样支持多线程并发访问,且可以有界。
- ConcurrentLinkedQueue:基于链接节点的无界线程安全队列,使用非阻塞算法实现。
2.5.3 哪些Queue实现类是线程安全的?
线程安全的Queue实现类包括:
- LinkedBlockingQueue
- ArrayBlockingQueue
- ConcurrentLinkedQueue
这些类通过内部同步机制或其他并发控制手段来保证多线程环境下的数据一致性和线程安全。
2.5.4 如何在使用非线程安全的Queue时保证其线程安全?
当使用非线程安全的Queue(如LinkedList、ArrayDeque等)时,可以通过以下几种方式保证其线程安全:
- 使用同步包装器:可以使用
Collections.synchronizedList(new LinkedList<>())
等方法将非线程安全的Queue包装成线程安全的,但这种方式在并发量较大时性能较低。 - 使用显式的锁:在访问Queue的代码块上使用显式的锁(如ReentrantLock或synchronized关键字)来同步对Queue的访问。
- 使用线程安全的Queue实现:直接选择线程安全的Queue实现类,如LinkedBlockingQueue、ArrayBlockingQueue或ConcurrentLinkedQueue。
2.5.5 请简述一下Queue在消息队列(Message Queue, MQ)中的应用?
在消息队列(MQ)中,Queue是消息存储和分发的核心组件。生产者将消息发送到指定的Queue中,而消费者则从Queue中拉取消息进行消费。Queue在MQ中扮演着缓冲区和调度器的角色,通过解耦生产者和消费者、实现流量削峰、提高系统可伸缩性和可靠性等目标。
例如,在RocketMQ中,Topic和Queue是消息存储和分发的两个关键概念。每个Topic可以包含多个Queue,Queue是实际存储消息数据的地方。生产者发送消息到指定的Topic,Broker根据配置将消息写入到Topic下的某个Queue。消费者则订阅感兴趣的Topic,并从其下的Queue中拉取消息进行消费。
2.5.6 Queue的容量限制是如何处理的?
对于Queue的容量限制,不同的实现类处理方式不同:
- 无界队列:如ConcurrentLinkedQueue和LinkedBlockingQueue(不指定容量时),它们没有容量限制,可以无限增长,直到耗尽系统资源。
- 有界队列:如ArrayBlockingQueue,在创建时需要指定容量。当队列达到容量上限时,如果再有元素入队,根据构造队列时指定的行为(如阻塞、抛出异常或返回特殊值),队列将进行相应的处理。
2.5.7 Queue在并发环境下的性能表现如何?
Queue在并发环境下的性能表现取决于其实现类和具体的使用场景。一般来说,线程安全的Queue实现类(如LinkedBlockingQueue、ArrayBlockingQueue)在并发环境下具有较好的性能表现,因为它们通过内部同步机制或其他并发控制手段来保证线程安全。然而,在高并发场景下,这些实现类可能会成为性能瓶颈。此时,可以考虑使用非阻塞的Queue实现类(如ConcurrentLinkedQueue)或结合其他并发工具(如ConcurrentHashMap、ReentrantLock等)来优化性能。