文章目录
- 1. 字符串拼接的底层原理
- 2. == 比较的是什么?
- 3. Strings1=newString("abc");这句话创建了几个字符串对象?
- 4. String#intern方法有什么作用?
- 5. String 为什么是不可变的?
- 6. 引用拷贝、深拷贝、浅拷贝
- 7. hashCode有什么用?
- 8. Java中的扩容机制
- 9. 多态调用成员的特点?
- 10. 自动装箱与拆箱了解吗?原理是什么?
- 11. 包装类型的缓存机制了解吗?
- 11. 为什么浮点数运算的时候会有精度丢失的风险?
- 12. 如何解决浮点数运算的精度丢失问题?
- 13. 超过long整整型的数据应该如何表示?
- 14. 为什么使用Objects.equals()而不是x.equals(y) ?
- 15. Java 泛型了解吗?什么是类型擦除?介绍一下常用的通配符?
- 16. 内部类了解吗?匿名内部类了解吗?
- 17. Java中序列化和反序列化是什么?
- 18. Java中参数类型传递是值传递还是引用传递?
- 19. 接口和抽象类有什么区别?
- 20. JRE 和 JDK的区别?
- 21. 什么是Java中的动态代理?
- 22. 使用过Java的反射机制吗?
- 23. Java的Optional类是什么?它有什么用?
- 24. 说说Java中HashMap的原理?
- 25. Java中有哪些集合类?
- 26. 说说 List,Set,Map 三者的区别?三者底层的数据结构?
- 27. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
- 28. HashMap 的长度为什么是 2 的幂次方
- 29. ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
- 30. Java 中的 CopyOnWriteArrayList 是什么?
- 31. 什么是线程和进程?线程与进程的关系,区别及优缺点?
- 32. 什么是上下文切换?
- 33. 什么是线程死锁?如何避免死锁?
- 34. 乐观锁和悲观锁了解吗?如何实现乐观锁?
- 35. 说说 sleep() 方法和 wait() 方法区别和共同点?
- 36. 讲一下 JMM(Java 内存模型)
- 37. volatile 关键字解决了什么问题?
- 38. synchronized 关键字的作用?
- 39. synchronized 关键字和 volatile 关键字的区别
- 40. synchronized 和 ReentrantLock 的区别?
- 41. Java 内存区域和 JMM 有何区别?
- 42. ThreadLocal 关键字的作用,内存泄露问题
- 43. Java线程池的原理?
- 44. 线程池有哪些拒绝策略?
- 45. 线程池有什么用?为什么不推荐使用内置线程池?
- 46. 实现 Runnable 接口和 Callable 接口的区别?
- 47. synchronized 关键字的底层原理?
- 48. 线程池有哪些参数?如何动态修改线程池参数?
- 49. CountDownLatch 有什么用?原理是什么?
- 50. AQS的原理是什么?
- 51. CyclicBarrier 有什么用?原理是什么?
1. 字符串拼接的底层原理
String str = "a" + "b";
Tip:这种方式创建字符串对象,生成的对象会在字符串常量池中,下次如果有相同值的对象需要创建,会复用这个对象。
拼接的时候没有变量,触发字符串优化机制,在编译的时候就确定结果了。编译器会直接认为这行代码是String str = “ab”;
如果拼接的时候有变量,jdk 8之前是像下图这样做的,每次拼接创建一个StringBuilder对象,变成字符串还要再生成一个String对象

jdk 8之后会先对拼接长度做一个预估,把要拼接的字符放到数组里,再去创建字符串对象(这种做法同样效率不高)
结论:如果多个字符串变量拼接,不要直接用+
StringBuffer比StringBuilder多了加锁操作,效率略低一些,但是是线程安全的
- 操作少量的数据: 适用 String
- 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
2. == 比较的是什么?
- 基本数据类型比的是值
- 引用数据类型比较的是地址值
3. Strings1=newString(“abc”);这句话创建了几个字符串对象?
-
字符串常量池中不存在 “abc”:会创建 2 个 字符串对象。一个在字符串常量池中,由 ldc 指令触发创建。一个在堆中,由 new String() 创建,并使用常量池中的 “abc” 进行初始化。
-
字符串常量池中已存在 “abc”:会创建 1 个 字符串对象。该对象在堆中,由 new String() 创建,并使用常量池中的 “abc” 进行初始化。
4. String#intern方法有什么作用?
- 常量池中已有相同内容的字符串对象:如果字符串常量池中已经有一个与调用 intern() 方法的字符串内容相同的 String 对象,intern() 方法会直接返回常量池中该对象的引用。
- 常量池中没有相同内容的字符串对象:如果字符串常量池中还没有一个与调用 intern() 方法的字符串内容相同的对象,intern() 方法会将当前字符串对象的引用添加到字符串常量池中,并返回该引用。
// s1 指向字符串常量池中的 "Java" 对象
String s1 = "Java";
// s2 也指向字符串常量池中的 "Java" 对象,和 s1 是同一个对象
String s2 = s1.intern();
// 在堆中创建一个新的 "Java" 对象,s3 指向它
String s3 = new String("Java");
// s4 指向字符串常量池中的 "Java" 对象,和 s1 是同一个对象
String s4 = s3.intern();
// s1 和 s2 指向的是同一个常量池中的对象
System.out.println(s1 == s2); // true
// s3 指向堆中的对象,s4 指向常量池中的对象,所以不同
System.out.println(s3 == s4); // false
// s1 和 s4 都指向常量池中的同一个对象
System.out.println(s1 == s4); // true
5. String 为什么是不可变的?
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
//...
}
- 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
- String 类被 final 修饰导致其在父类中也不能被重新赋值。
6. 引用拷贝、深拷贝、浅拷贝

7. hashCode有什么用?
hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
- 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
- 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。
- 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。
8. Java中的扩容机制
-
ArrayList
初始容量:默认10(使用无参构造时)。
扩容策略:
新容量 = 旧容量 + 旧容量 / 2(即扩容1.5倍)。
若扩容后仍不足,则直接扩容到所需的最小容量。 -
HashMap
初始容量:默认16(必须为2的幂次)。
负载因子:默认0.75(权衡空间与时间效率)。
扩容策略:
新容量 = 旧容量 × 2(保持为2的幂次)。 -
StringBuilder / StringBuffer
初始容量:默认16(字符数组长度)。
扩容策略:
新容量 = max(旧容量 × 2 + 2, 所需的最小容量)。
9. 多态调用成员的特点?
- 变量调用:编译看左边,运行也看左边
- 方法调用:编译看左边,运行看右边
10. 自动装箱与拆箱了解吗?原理是什么?
什么是自动拆装箱?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
Integer i = 10 等价于 Integer i = Integer.valueOf(10);
int n = i 等价于 int n = i.intValue();
如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
11. 包装类型的缓存机制了解吗?
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 TRUE or FALSE。
两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。
所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
11. 为什么浮点数运算的时候会有精度丢失的风险?
计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,有些小数,比如说十进制下的 0.2 在用二进制表示的时候长度是无限的,这时候在计算机存储的时候就会被截断,下次读取的时候就会有精度丢失
12. 如何解决浮点数运算的精度丢失问题?
BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。
13. 超过long整整型的数据应该如何表示?
BigInteger 内部使用 int[] 数组来存储任意大小的整形数据。
14. 为什么使用Objects.equals()而不是x.equals(y) ?
x.equals(y)中x可能是空指针,用Objects.equals()更安全
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
15. Java 泛型了解吗?什么是类型擦除?介绍一下常用的通配符?
泛型
Java 泛型是一种在编译时提供类型安全检查的机制,允许开发者编写更通用、可读且类型安全的代码。
泛型的作用:
-
类型安全:通过编译时检查,避免运行时类型转换错误(如 ClassCastException)。
-
代码复用:编写可处理多种数据类型的通用代码(如集合框架 List)。
-
消除强制转换:直接从泛型容器中获取对象时无需显式类型转换。
类型擦除
Java 泛型在编译期间会移除类型参数信息(即“擦除”),替换为原始类型(如 Object)或类型边界。
擦除规则
无界类型参数(如 ):替换为 Object。
// 编译前
class Box<T> { T value; }
// 编译后
class Box { Object value; }
有界类型参数(如 ):替换为边界类型(Number)。
// 编译前
class Box<T extends Number> { T value; }
// 编译后
class Box { Number value; }
擦除的影响
-
运行时无法获取泛型类型:例如 List 运行时仅为 List。
-
无法创建泛型数组:如 new T[] 会导致编译错误。
-
重载冲突:因擦除后参数类型相同,如 void print(List list) 和 void print(List list) 会编译报错。
通配符
通配符用于增强泛型的灵活性,常见于方法参数或变量声明。
1. 上界通配符 <? extends T>
作用:接受 T 或其子类型,支持安全读取,但禁止写入(除 null)。
场景:从泛型容器中读取数据(遵循 PECS 原则的 Producer)。
void printList(List<? extends Number> list) {
for (Number num : list) { // 安全读取
System.out.println(num);
}
// list.add(1); // 编译错误:无法添加具体类型
}
2. 下界通配符 <? super T>
作用:接受 T 或其父类型,支持安全写入,但读取时需强制转换。
场景:向泛型容器写入数据(遵循 PECS 原则的 Consumer)。
void addNumbers(List<? super Integer> list) {
list.add(1); // 安全写入 Integer
list.add(2);
// Integer num = list.get(0); // 需强制转换
}
3. 无界通配符 <?>
作用:表示未知类型,仅支持与 Object 相关的操作,支持安全读取,但禁止写入(除 null)。<?>和<? extends Object>在语义上是等价的。
场景:不依赖具体类型的操作(如判断非空)。
boolean isEmpty(List<?> list) {
return list.size() == 0;
}
通配符和类型参数的使用位置
类型参数 T 用于声明泛型类、接口或方法。
// 泛型类
class Box<T> {
private T value;
}
// 泛型方法
public <T> T getFirst(List<T> list) {
return list.get(0);
}
通配符 ? 用于使用泛型的变量或方法参数,表示未知类型。
// 方法参数中使用通配符
void printList(List<?> list) {
for (Object elem : list) {
System.out.println(elem);
}
}
// 变量声明中使用通配符
List<? extends Number> numbers = new ArrayList<Integer>();
16. 内部类了解吗?匿名内部类了解吗?
内部类是定义在另一个类内部的类
匿名内部类没有类名,通常在实例化接口或抽象类时直接定义实现,常见用法:
// 实现接口
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Running in anonymous class");
}
};
// 继承抽象类
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("Thread running");
}
};
17. Java中序列化和反序列化是什么?
序列化
是将对象转换为字节流的过程,这样对象可以通过网络传输、持久化存储或者缓存。Java提供了 java.io.Serializable接口来支持序列化,只要类实现了这个接口,就可以将该类的对象进行序列化。
反序列化
是将字节流重新转换为对象的过程,即从存储中读取数据并重新创建对象。
其他
- Java 序列化关键类和接口:objectOutputStream用于序列化,ObjectInputStream用于反序列化。类必须实现Serializable接口才能被序列化。
- transient关键字:在序列化过程中,有些字段不需要被序列化,例如敏感数据,可以使用transient关键字标记不需要序列化的字段。
- serialVersionUID:每个Serializable类都应该定义一个serialVersionUID,用于在反序列化时验证版本一致性。如果没有明确指定,Java会根据类的定义自动生成一个UID,版本不匹配可能导致反序列化失败。
即使类结构发生变化(如新增非关键字段),通过保持相同的serialVersionUID仍可兼容旧数据。
18. Java中参数类型传递是值传递还是引用传递?
Java中将实参传递给方法(或函数)的方式是值传递
- 如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。
- 如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本。
19. 接口和抽象类有什么区别?
接口和抽象类在设计动机上有所不同。
- 接口的设计是
自上而下的。我们知晓某一行为,于是基于这些行为约束定义了接口,一些类需要有这些行为,因此实现对应的接口。 - 抽象类的设计是
自下而上的。我们写了很多类,发现它们之间有共性,有很多代码可以复用,因此将公共逻辑封装成一个抽象类,减少代码余。
20. JRE 和 JDK的区别?
JRE(JavaRuntimeEnvironment)指的是Java运行环境,包含了JVM、核心类库和其他支持运行Java程序的文件。
JDK(JavaDevelopmentKit)可以视为JRE的超集,是用于开发Java程序的完整开发环境,它包含了JRE,以及用于开发、调试和监控Java应用程序的工具。
21. 什么是Java中的动态代理?
【2025版】JDK动态代理与CGLIB代理使用方法
代理可以看作是调用目标的一个包装,通常用来在调用真实的目标之前进行一些逻辑处理,消除一些重复的代码。
Java中的动态代理是一种在运行时创建代理对象的机制。动态代理允许程序在运行时决定代理对象的行为,而不需要在编译时确定。它通过代理模式为对象提供了一种机制,使得可以在不修改目标对象的情况下对其行为进行增强或调整。
静态代理指的是我们预先编码好一个代理类,而动态代理指的是运行时生成代理类。
22. 使用过Java的反射机制吗?
【25年最新版】一文带你搞懂JAVA反射机制,无压力入门JAVA反射
23. Java的Optional类是什么?它有什么用?
Java 的 Optional 类是在 Java 8 中引入的一个容器类,位于 java.util 包中。它的核心目的是更优雅地处理可能为 null 的值,减少代码中对 null 的直接检查(如 if (obj == null)),从而降低 NullPointerException 的风险,同时提高代码的可读性和健壮性。
使用场景:
与 Stream 结合实现更安全的函数式操作:
List<String> names = users.stream()
.map(user -> Optional.ofNullable(user.getName()).orElse("Unknown"))
.collect(Collectors.toList());
24. 说说Java中HashMap的原理?
HashMap是基于哈希表的数据结构,用于存储键值对(key-value)。其核心是将键的哈希值映射到数组索引位置,通过数组+链表(在Java8及之后是数组+链表+红黑树)来处理哈希冲突。
HashMap使用键的hashCode()方法计算哈希值,并通过indexFor方法(JDK1.7及之后版本移除了这个方法,直接使用(n-1)& hash)确定元素在数组中的存储位置。哈希值是经过一定扰动处理的,防止哈希值分布不均匀,从而减少冲突。当不同的键映射到同一桶时,通过链表或红黑树解决冲突。链表长度≥8且数组长度≥64时,链表转为红黑树。当红黑树节点数≤6时,树退化为链表。
HashMap的默认初始容量为16,负载因子为0.75。也就是说,当存储的元素数量超过16×0.75=12个时,HashMap 会触发扩容操作,容量x2并重新分配元素位置。这种扩容是比较耗时的操作,频繁扩容会影响性能。
25. Java中有哪些集合类?
常见的:

Java的集合框架提供了丰富的类和接口来存储和操作数据,主要分为两大核心接口:Collection(单列集合)和Map(键值对集合)。
一、Collection 接口
- List(有序、可重复)
ArrayList:基于动态数组,查询快,增删慢,线程不安全。
LinkedList:基于双向链表,增删快,查询慢,同时实现了Deque接口。
Vector:线程安全的动态数组(同步实现),性能较差,已逐渐被替代。
CopyOnWriteArrayList(并发包):线程安全的List,写操作时复制数组,适合读多写少场景。 - Set(无序、不可重复)
HashSet:基于哈希表,快速查找,允许null元素。
LinkedHashSet:继承HashSet,维护插入顺序的链表。
TreeSet:基于红黑树,元素按自然顺序或自定义比较器排序。
CopyOnWriteArraySet(并发包):线程安全的Set,基于CopyOnWriteArrayList实现。
EnumSet:专为枚举类型设计的高效集合。 - Queue/Deque(队列)
LinkedList:同时实现了Queue接口,可用作普通队列或双端队列。
PriorityQueue:基于堆结构的优先级队列,元素按优先级出队。
ArrayBlockingQueue(并发包):基于数组的有界阻塞队列。
LinkedBlockingQueue(并发包):基于链表的可选有界阻塞队列。
ConcurrentLinkedQueue(并发包):非阻塞线程安全队列。
二、Map 接口(键值对)
HashMap:基于哈希表,允许null键/值,线程不安全。
LinkedHashMap:维护插入顺序或访问顺序(适合实现LRU缓存)。
TreeMap:基于红黑树,键按自然顺序或自定义比较器排序。
Hashtable:线程安全的哈希表(同步实现),已逐渐被ConcurrentHashMap替代。
ConcurrentHashMap(并发包):高并发优化的线程安全Map,分段锁/乐观锁实现。
WeakHashMap:键为弱引用,适合缓存场景,GC时自动回收无引用键。
IdentityHashMap:使用==代替equals比较键,适用于特殊需求。
EnumMap:专为枚举类型键设计的高效Map。
26. 说说 List,Set,Map 三者的区别?三者底层的数据结构?
List、Set、Map的主要区别在于数据存储特性与用途:List是有序且允许重复元素的序列,底层通常基于动态数组(如ArrayList)或双向链表(如LinkedList)实现;Set是无序且元素唯一的集合,常用哈希表 + 链表/红黑树(HashSet)实现快速查找,或用红黑树(TreeSet)实现有序存储;Map是以键值对形式存储数据的映射结构,通过哈希表 + 链表/红黑树(HashMap)实现键的快速定位,也可用红黑树(TreeMap)保持键的有序性,其核心机制是通过键的哈希值或比较规则保证键的唯一性,而值允许重复。
27. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
- HashSet基于哈希表+链表/红黑树实现,元素无序且插入、查询的时间复杂度接近O(1),性能最优但无法保证遍历顺序;
- LinkedHashSet在哈希表基础上通过维护双向链表保留了元素的插入顺序,因此在迭代时会按照元素添加的先后顺序输出,其性能略低于HashSet但高于TreeSet;
- TreeSet则基于红黑树实现,元素始终按照自然顺序或自定义比较器排序,支持范围查询和有序操作(如ceiling()、floor()),但插入和删除的时间复杂度为O(log n),适用于需要排序的场景。
28. HashMap 的长度为什么是 2 的幂次方
HashMap采用2的n次方倍作为容量,主要是为了提高哈希值的分布均匀性和哈希计算的效率。
- HashMap通过(n-1)&hash来计算元素存储的索引位置,这种位运算只有在数组容量是2的n次方时才能确保索引均匀分布。位运算的效率高于取模运算(hash%n),提高了哈希计算的速度。
- 且当HashMap扩容时,通过容量为2的n次方,扩容时只需通过简单的位运算判断是否需要迁移,这减少了重新计算哈希值的开销,提升了rehash的效率。
29. ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
ConcurrentHashMap 在 Java 8 及之后的版本中,通过 CAS 无锁化操作、synchronized 细粒度桶锁、多线程协同扩容机制 以及 volatile 内存可见性 的结合实现线程安全:底层采用 Node 数组与链表/红黑树结构,插入空桶时通过 CAS 竞争头节点,操作非空桶时仅对当前桶头节点加 synchronized 锁,避免全局锁竞争;扩容阶段通过 ForwardingNode 标记迁移状态并支持多线程分块协作迁移数据,读操作直接访问 volatile 字段保证无锁高效查询,同时利用分片计数(LongAdder 思想)降低 size 统计的竞争,最终在保证线程安全的同时,通过最小化锁粒度、无锁化尝试及并发扩容优化,实现高吞吐量的并发读写性能。
30. Java 中的 CopyOnWriteArrayList 是什么?
CopyonwriteArrayList是Java的一个线程安全的动态数组实现,属于java.util.concurrent包,它通过写时复制机制,即在每次修改(写入)操作时,复制原始数组的内容来保证线程安全。由于写操作涉及复制整个数组,所以它的写操作开销较大,但读取操作则完全无锁。这使得CopyOnWriteArrayList适合于读多写少的场景。
31. 什么是线程和进程?线程与进程的关系,区别及优缺点?
进程是操作系统资源分配的基本单位。
线程是进程内的执行单元,是操作系统调度的基本单位。
一个进程可以包含多个线程,线程是进程的组成部分。
32. 什么是上下文切换?
上下文切换(Context Switching) 是操作系统在多任务环境中切换执行不同进程或线程时,保存当前任务状态并加载下一任务状态的过程。它是实现并发执行的核心机制,但会带来一定的性能开销。
33. 什么是线程死锁?如何避免死锁?
线程死锁是指两个或多个线程在运行过程中因争夺资源而陷入互相等待的状态,导致所有线程都无法继续执行。
死锁的四个必要条件(必须同时满足才会发生死锁):
- 互斥条件:资源只能被一个线程独占使用。
- 请求与保持条件:线程已持有至少一个资源,同时请求其他资源。
- 不可剥夺条件:资源只能由持有者主动释放,不能被强制剥夺。
- 循环等待条件:存在线程之间的等待环路(如T1等待T2,T2等待T1)。
通过破坏死锁的四个必要条件之一即可避免:
- 破坏互斥条件
方法:将独占资源改为可共享资源(如只读文件)。
局限:许多资源(如写操作、数据库连接)无法共享。 - 破坏请求与保持条件
一次性申请所有资源:线程在开始执行前申请全部所需资源。 - 破坏不可剥夺条件
超时释放:若线程无法在指定时间内获取资源,则主动释放已持有资源。 - 破坏循环等待条件
固定资源申请顺序:所有线程按统一顺序申请资源。
34. 乐观锁和悲观锁了解吗?如何实现乐观锁?
| 锁类型 | 基本思想 | 适用场景 |
|---|---|---|
| 悲观锁 | 假设并发冲突一定会发生,因此先加锁再操作数据(如synchronized、ReentrantLock)。 | 写操作频繁,冲突概率高的场景。 |
| 乐观锁 | 假设并发冲突很少发生,因此先操作数据,提交时再检测冲突(如CAS、版本号机制)。 | 读多写少,冲突概率低的场景。 |
悲观锁的实现:
典型方式:通过互斥锁(如synchronized关键字、ReentrantLock)直接阻塞其他线程。
乐观锁的实现:
乐观锁的核心是冲突检测,常见实现方式有两种:
- 版本号机制(Version Control)
- 数据表中增加一个版本号字段(如version)。
- 读取数据时记录当前版本号。
- 提交修改时,检查版本号是否未被修改,若未被修改则更新数据并递增版本号。
- CAS(Compare-And-Swap)
原理:通过CPU原子指令(如x86的CMPXCHG)直接比较内存值与预期值,若一致则更新。
35. 说说 sleep() 方法和 wait() 方法区别和共同点?
在 Java 中,sleep() 是 Thread 类的静态方法,调用后线程休眠指定时间不释放锁,通常用于单纯暂停线程执行;而 wait() 是 Object 类的实例方法,必须在同步块中调用,会释放对象锁并让线程等待,直到其他线程调用 notify()/notifyAll() 或超时,主要用于线程间协作。两者均会阻塞线程且可被中断,但 sleep() 不涉及锁的释放与竞争,而 wait() 依赖锁机制实现线程通信。
36. 讲一下 JMM(Java 内存模型)
Java Memory Model (JMM) 是 Java 多线程环境下,规范线程如何与主内存、工作内存交互的抽象模型,目的是解决多线程的可见性、有序性、原子性问题。其核心规则如下:
- 主内存与工作内存
-
每个线程有自己的工作内存(缓存/寄存器),保存主内存变量的副本。
-
线程操作变量时,需先从主内存拷贝到工作内存,修改后刷新回主内存。
- 原子性、可见性、有序性
-
原子性:对基本类型(如 int)的读写是原子的,但 long/double 可能非原子(32 位 JVM 中)。
-
可见性:线程修改共享变量后,其他线程是否能立即看到修改。
-
有序性:代码执行顺序可能被编译器和处理器优化重排(指令重排序)。
关于原子性:
被 synchronized 包裹的代码块中的所有操作作为一个整体执行,不会被其他线程中断。
volatile 关键字不保证所有操作的原子性,它仅能保证对变量的单次读/写操作是原子的。对于复合操作(如自增 i++ 或先读后写),仍需依赖其他机制(如锁或原子类)来确保原子性。
关于可见性:
- 线程退出 synchronized 代码块时,会将工作内存中的变量刷新到主内存。
- 线程进入 synchronized 代码块时,会清空工作内存,强制从主内存重新加载变量。
- 写 volatile 变量会直接刷新到主内存,读 volatile 变量会从主内存重新加载,确保所有线程看到最新值。
关于有序性:
synchronized关键字通过锁机制确保代码块串行执行。
volatile 关键字通过内存屏障(如 StoreStore、LoadLoad)阻止指令重排。
关于Happens-Before 原则:
Happens-Before 原则是 JMM(Java 内存模型)的核心规则,它定义了多线程环境下操作之间的可见性和顺序性约束,是 JMM 实现内存一致性的理论基础。
Happens-Before 规则明确了多线程操作之间的因果顺序,确保:
- 可见性:若操作 A Happens-Before 操作 B,则 A 对共享变量的修改对 B 可见。
- 有序性:操作 A 的结果必须在操作 B 执行前完成,禁止破坏因果关系的重排序。
37. volatile 关键字解决了什么问题?
-
可见性问题
写 volatile 变量会直接刷新到主内存,读 volatile 变量会从主内存重新加载,确保所有线程看到最新值。 -
有序性问题
通过插入内存屏障禁止指令重排序,保证 volatile 变量的操作顺序性。
38. synchronized 关键字的作用?
synchronized 是 Java 中实现线程同步的关键字,用于解决多线程并发场景下的线程安全问题。其核心作用包括:
- 互斥性(Mutual Exclusion)
确保同一时刻最多只有一个线程可以执行被 synchronized 修饰的代码,其他线程必须等待锁释放。 - 原子性(Atomicity)
保证被保护的代码块或方法的操作是不可分割的,避免线程切换导致的数据不一致。 - 可见性(Visibility)
线程在进入 synchronized 块时,会强制从主内存重新加载变量;退出时会将修改后的值刷新到主内存,遵循 Happens-Before 原则。
39. synchronized 关键字和 volatile 关键字的区别
synchronized:解决原子性、可见性、有序性,但性能开销较大。
volatile:仅解决可见性和有序性,开销小,适合简单场景。
40. synchronized 和 ReentrantLock 的区别?
synchronized 和 ReentrantLock 都是 Java 中解决线程安全的同步机制,但有以下区别:
- 实现机制:synchronized 是 JVM 隐式管理的锁,ReentrantLock 是 JDK 显式锁,基于 AQS 实现。
- 灵活性:ReentrantLock 支持可中断锁、公平锁、多条件变量及尝试获取锁,功能更丰富。
- 锁释放:synchronized 自动释放,ReentrantLock 需手动调用 unlock()。
- 性能:JDK 1.6 后两者性能接近,但 ReentrantLock 在高竞争场景更灵活。
- 适用场景:简单同步推荐 synchronized;复杂场景(如需要公平性、超时控制)选择 ReentrantLock。
41. Java 内存区域和 JMM 有何区别?
-
Java 内存区域
是 JVM 运行时数据区域的物理划分,定义了 JVM 在运行过程中如何管理内存(分配、使用、回收)。例如:- 堆(Heap):存储对象实例。
- 方法区(Method Area):存储类元数据、常量池等。
- 虚拟机栈(VM Stack):存储方法调用的栈帧、局部变量等。
- 本地方法栈(Native Method Stack):支持 Native 方法调用。
- 程序计数器(PC Register):记录线程执行的位置。
-
JMM(Java Memory Model)
是 Java 定义的一套多线程内存访问的规范,目的是解决多线程环境下共享变量的可见性、有序性和原子性问题。例如:- 规定线程如何通过主内存(Main Memory)和工作内存(Working Memory)交互。
- 定义了 volatile、synchronized 等关键字的语义。
42. ThreadLocal 关键字的作用,内存泄露问题
ThreadLocal用于为每个线程维护独立的变量副本,例如在Web开发中存储用户会话信息,确保线程安全。其潜在的内存泄露问题源于ThreadLocalMap的Entry设计:key是弱引用,而value是强引用。当外部强引用失效后,key会被GC回收,但value仍被Entry强引用,导致无法回收。若线程长期存活(如线程池),残留的value会引发内存泄露。解决方法是每次使用后调用remove()清理,并尽量将ThreadLocal声明为static,避免key提前被回收。
43. Java线程池的原理?
线程池是一种池化技术,用于预先创建并管理一组线程,避免频繁创建和销毁线程的开销,提高性能和响应速度。
它几个关键的配置包括:核心线程数、最大线程数、空闲存活时间、工作队列、拒绝策略。
主要工作原理如下:
- 默认情况下线程不会预创建,任务提交之后才会创建线程(不过设置prestartAllCoreThreads可以
预创建核心线程)。 - 当核心线程满了之后不会新建线程,而是把任务堆积到工作队列中。
- 如果工作队列放不下了,然后才会新增线程,直至达到最大线程数。
- 如果工作队列满了,然后也已经达到最大线程数了,这时候来任务会执行拒绝策略。
- 如果线程空闲时间超过空闲存活时间,并且当前线程数大于核心线程数的则会销毁线程,直到线程数等于核心线程数(设置allowCoreThreadTimeOut为true可以回收核心线程,默认为false)。
44. 线程池有哪些拒绝策略?
一共提供了4种:
1)AbortPolicy:当任务队列满且没有线程空闲,此时添加任务会直接抛出RejectedExecutionException错误,这也是默认的拒绝策略。适用于必须通知调用者任务未能被执行的场景。
2)CallerRunsPolicy:当任务队列满且没有线程空闲,此时添加任务由即调用者线程执行。适用于希望通过减缓任务提交速度来稳定系统的场景。
3)DiscardOldestPolicy:当任务队列满且没有线程空闲,会删除最早的任务,然后重新提交当前任务。适用于希望丢弃最旧的任务以保证新的重要任务能够被处理的场景。
4)DiscardPolicy:直接丢弃当前提交的任务,不会执行任何操作,也不会抛出异常。适用于对部分任务丢弃没有影响的场景,或系统负载较高时不需要处理所有任务。
45. 线程池有什么用?为什么不推荐使用内置线程池?
线程池的核心作用
- 资源复用,降低开销
线程创建和销毁成本高昂(涉及内存分配、上下文切换等)。线程池通过复用固定数量的线程执行任务,避免频繁创建销毁线程的开销。 - 控制并发量,防止资源耗尽
通过限制最大线程数和任务队列容量,避免无限制创建线程导致内存溢出(OOM)或 CPU 过载。 - 统一管理任务队列和调度策略
提供任务排队机制(如阻塞队列)和拒绝策略(如丢弃或抛出异常),确保任务有序执行或优雅降级。 - 提升响应速度
当任务到达时,线程池中可能已有空闲线程可直接处理,无需等待线程创建。
为什么不推荐使用内置线程池?
- 潜在资源耗尽风险
内置实现默认使用无界队列,在高并发场景下,可能瞬间创建大量线程,导致 CPU 和内存资源耗尽。 - 灵活性不足
内置线程池的参数(核心线程数、最大线程数、队列类型、拒绝策略等)固定,无法根据业务需求调整
46. 实现 Runnable 接口和 Callable 接口的区别?
Runnable 的任务没有返回值,适合不关心结果的异步操作(如日志记录、状态更新)。
Callable 的任务返回泛型值,适合需要获取执行结果的场景(如计算、数据查询)。
47. synchronized 关键字的底层原理?
synchronized关键字的底层实现主要依赖于对象内部的监视器锁(Monitor)和对象头中的Mark Word结构。当一个线程进入synchronized修饰的代码块或方法时,JVM会通过monitorenter指令尝试获取对象的监视器锁,此时对象头中的Mark Word会被替换为指向锁记录的指针或锁标志位。如果锁未被其他线程占用,当前线程会成功获取锁并执行同步代码;如果锁已被占用,线程会进入阻塞状态,直到持有锁的线程执行monitorexit指令释放锁。在Java 6之后,synchronized的锁机制进行了优化,引入了偏向锁、轻量级锁和重量级锁的升级过程。初始时,锁会偏向第一个访问的线程,通过CAS操作记录线程ID实现无竞争快速获取;当出现轻微竞争时,锁会升级为轻量级锁,通过自旋尝试避免线程阻塞;若竞争加剧,则进一步升级为重量级锁,此时未获取锁的线程会被挂起,依赖操作系统底层的互斥量实现同步。
48. 线程池有哪些参数?如何动态修改线程池参数?
线程池核心参数
1) 核心线程数(corePoolSize)
线程池中保持活动状态的最小线程数,即使空闲也不会被回收(除非设置allowCoreThreadTimeOut为true)。
2) 最大线程数(maximumPoolSize)
线程池允许创建的最大线程数。当任务队列满且当前线程数小于最大线程数时,会创建新线程。
3) 任务队列(workQueue)
存放待执行任务的阻塞队列。常见类型:
- 有界队列(如ArrayBlockingQueue):防止资源耗尽,但可能触发拒绝策略。
- 无界队列(如LinkedBlockingQueue):任务无限堆积,可能导致内存溢出。
- 同步移交队列(如SynchronousQueue):任务不排队,直接交给线程处理。
4) 线程存活时间(keepAliveTime)
当线程数超过核心线程数时,多余的空闲线程在终止前等待新任务的时间。
5) 线程工厂(threadFactory)
用于创建线程,可自定义线程名称、优先级等(如通过ThreadFactory接口)。
6) 拒绝策略(RejectedExecutionHandler)
当任务队列满且线程数达到最大值时的处理策略,常见策略包括:
- AbortPolicy(默认):抛RejectedExecutionException。
- CallerRunsPolicy:由提交任务的线程直接执行任务。
- DiscardPolicy:静默丢弃任务。
- DiscardOldestPolicy:丢弃队列中最旧的任务,然后重新提交。
动态修改线程池参数
原生 ThreadPoolExecutor 的动态参数调整Java的ThreadPoolExecutor提供以下方法直接修改参数:
- 核心线程数:setCorePoolSize(int)
- 最大线程数:setMaximumPoolSize(int)
- 线程空闲超时:setKeepAliveTime(long, TimeUnit)
注意事项:
- 调大核心线程数时,线程池会立即创建新线程处理任务。
- 调低核心线程数时,多余线程会在空闲时被回收(需超过keepAliveTime)。
最大线程数必须 ≥ 核心线程数,否则会抛异常。
49. CountDownLatch 有什么用?原理是什么?
CountDownLatch 是 Java 并发包(java.util.concurrent)中的一个工具类,用于协调多个线程之间的同步。它的核心功能是允许一个或多个线程等待其他线程完成操作后再继续执行。
主要用途
1) 主线程等待子线程完成任务
例如:主线程需要等待所有子线程初始化完成后,再继续执行后续逻辑。
2) 子线程等待主线程发出信号
例如:多个子线程等待主线程发出“开始执行”的信号后再并行执行任务。
3) 资源初始化等待
例如:确保某些资源(如数据库连接、服务等)全部初始化完成后再启动应用。
public class Demo {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
System.out.println("子线程完成任务");
latch.countDown(); // 计数器减1
}).start();
}
latch.await(); // 主线程等待所有子线程完成任务
System.out.println("所有子线程已完成任务,主线程继续执行");
}
}
底层实现
CountDownLatch 基于 AQS(AbstractQueuedSynchronizer) 实现,通过共享锁机制控制同步:
- 计数器值对应 AQS 的 state 变量。
- await() 方法会尝试获取共享锁,如果 state > 0,线程进入阻塞队列。
- countDown() 方法会释放共享锁,减少 state 值。当 state 归零时,唤醒所有等待线程。
50. AQS的原理是什么?
AQS(AbstractQueuedSynchronizer)是Java并发包中构建锁和同步器的核心框架,其原理基于一个双向队列(CLH队列)和状态变量实现线程的阻塞与唤醒。AQS通过一个volatile的int类型state变量表示同步状态,子类通过重写tryAcquire和tryRelease等方法定义获取与释放状态的逻辑。当线程获取资源失败时,AQS会将其封装为Node节点加入队列并挂起,通过自旋或LockSupport.park()实现阻塞;当持有资源的线程释放时,会唤醒后继节点线程重新尝试获取状态。AQS通过这种机制实现了公平与非公平的同步策略,支持独占(如ReentrantLock)和共享(如Semaphore)两种模式,内部通过CAS操作保证线程安全,从而避免了竞态条件。
51. CyclicBarrier 有什么用?原理是什么?
CyclicBarrier 是 Java 中用于多线程同步的工具类,其核心作用是让一组线程相互等待,直到所有线程都到达某个屏障点(Barrier Point),然后一起继续执行。
- 初始化时指定等待的线程数 N。
- 每个线程调用 await() 时,计数器减 1,并进入阻塞状态。
- 当计数器减至 0 时,所有阻塞线程被唤醒,继续执行。
2970

被折叠的 条评论
为什么被折叠?



