Java并发面试题
Java 并发面试题
基础
什么是 java 中的线程安全
在 Java 中,线程安全是指一个类或对象在多线程环境下能够正确执行,不会出现数据不一致或其他异常。其核心在于处理共享可变状态的访问。
说说线程的几种创建方式
继承Thread类:
重写
run()
方法。创建子类对象并调用
start()
方法启动线程。
实现Runnable接口
实现
run()
方法。将实现类对象作为参数传递给
Thread
构造器,调用start()
启动线程。
实现 Callable 接口 + FutureTask
- 实现
Callable<V>
接口,指定返回值类型。 - 实现
call()
方法,该方法有返回值。 - 使用
FutureTask<V>
包装Callable
对象。 - 将
FutureTask
对象作为参数传递给Thread
构造器,调用start()
启动线程。 - 通过
FutureTask.get()
获取线程执行结果(可能阻塞)。
- 实现
使用线程池(ExecutorService)
- 创建线程池(如
Executors.newFixedThreadPool()
)。 - 提交
Runnable
或Callable
任务到线程池。 - 通过
Future
获取任务结果(若有)。 - 关闭线程池。
- 创建线程池(如
线程的生命周期
Java 线程的生命周期分为 6 种状态,由 Thread.State 枚举定义:
状态名称 | 说明 |
---|---|
NEW(新建) | 线程对象已创建,但未调用 start() 方法(未启动)。 |
RUNNABLE(可运行) | 线程已调用 start() 方法,正在 JVM 中运行,或等待 CPU 调度(包含 “就绪” 和 “运行中” 两种情况)。 |
BLOCKED(阻塞) | 线程因竞争 对象锁 而阻塞(如进入 synchronized 块 / 方法时未获取到锁)。 |
WAITING(无限等待) | 线程无超时地等待其他线程的特定操作(如 wait() 、join() 等)。 |
TIMED_WAITING(超时等待) | 线程有超时时间地等待(如 sleep(1000) 、wait(1000) 等,超时后自动唤醒)。 |
TERMINATED(终止) | 线程执行完毕(run() 方法结束)或因异常退出。 |
什么是线程上下文切换
线程上下文切换是操作系统实现多线程并发的基础,通过保存和恢复线程状态,让 CPU 能在多个线程间快速切换。但它存在固有开销,合理控制线程数量、减少不必要的阻塞(如避免频繁 I/O、优化锁竞争)是降低切换开销、提升并发性能的关键。
线程间有哪些通讯方式
在Java中,线程之间的通讯就是指多个线程协同工作。
线程之间传递信息的方式有多种,比如说使用 volatile 和 synchronized 关键字共享对象、使用 wait()
和 notify()
方法实现生产者-消费者模式、使用 Exchanger 进行数据交换、使用 Condition 实现线程间的协调等。
说说 sleep 和 wait 的区别
维度 | sleep() | wait() |
---|---|---|
所属类 | Thread.sleep() (静态方法) |
Object.wait() (实例方法) |
释放锁 | ❌ 不释放持有的锁 | ✅ 释放持有的对象锁(需在synchronized 中调用) |
唤醒方式 | 时间到期自动唤醒,或被interrupt() 中断 |
需其他线程调用notify() /notifyAll() 唤醒 |
使用场景 | 单纯暂停线程执行(如模拟耗时操作) | 线程间协作(如生产者 - 消费者模型中的条件等待) |
调用位置 | 可在任何代码中调用 | 必须在synchronized 块 / 方法中调用 |
ThreadLocal
ThreadLocal 是什么?
ThreadLocal
是多线程编程中的一个特殊工具,用于为每个使用它的线程都独立存储一份数据副本。每个线程都可以独立地修改自己的副本,而不会影响其他线程的数据,从而实现线程间的数据隔离。
说说对 ThreadLocal 的理解
- 每个
Thread
对象内部持有ThreadLocalMap
(哈希表结构)成员变量(threadLocals
)。 ThreadLocalMap
是ThreadLocal
的一个静态内部类,它内部维护了一个Entry
数组,Entry 继承了 WeakReference。Key
为ThreadLocal
实例(弱引用),Value
为线程变量副本(强引用)。- 数据访问:
- 调用
threadLocal.set(value)
时,以当前ThreadLocal
实例为 Key,存入当前线程的ThreadLocalMap
。 - 调用
threadLocal.get()
时,从当前线程的Map
中通过 Key 取出 Value。
- 调用
- 哈希优化:Key 的哈希值由黄金分割数 0x61c88647 生成,减少哈希冲突。
- 冲突解决:采用 开放定址法(线性探测),而非链表。
说说 ThreadLocal 的内存泄漏问题
- 原因:Entry 的 Key(ThreadLocal)是弱引用,但 Value 是强引用。如果
ThreadLocal
实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致ThreadLocalMap
中对应的 key 变为null
,但 Value 仍被线程强引用。 - 后果:若线程长期运行(如线程池场景), Value 无法回收,会导致内存泄漏。
- 解决方案:
- 每次使用后 **必须调用
remove()
**(在finally
块中); - 将 ThreadLocal 声明为
static final
,避免实例被回收。”
- 每次使用后 **必须调用
如何跨线程传递 ThreadLocal 的值?
ThreadLocal
无法直接跨线程传递值,因为 ThreadLocal 变量存储在每个线程的 ThreadLocalMap 中。
我们可以使用 InheritableThreadLocal
来解决这个问题。
InheritableThreadLocal:
是
ThreadLocal
的子类,专门用于在父子线程间传递值。当子线程被创建时,会自动复制父线程中InheritableThreadLocal
的值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class InheritableThreadLocalDemo {
private static final InheritableThreadLocal<String> context = new InheritableThreadLocal<>();
public static void main(String[] args) {
// 在主线程中设置值
context.set("主线程数据");
// 创建子线程
Thread childThread = new Thread(() -> {
// 子线程可以获取到父线程设置的值
System.out.println("子线程获取值: " + context.get()); // 输出: 主线程数据
});
childThread.start();
}
}局限性
- 仅适用于直接创建的子线程:如果使用线程池(如
ExecutorService
),由于线程是复用的,无法自动复制InheritableThreadLocal
的值。 - 值复制是单向的:子线程无法修改父线程中的值,反之亦然。
- 仅适用于直接创建的子线程:如果使用线程池(如
Java内存模型
说说对Java内存模型的理解
Java 内存模型(Java Memory Model, JMM)是一种抽象规范,用于定义多线程环境下变量的访问规则,解决以下问题:
- 内存可见性:确保一个线程对共享变量的修改能及时被其他线程看到。
- 指令重排序:编译器和 CPU 会对指令重排序优化,导致代码执行顺序与编写顺序不一致。
- 原子性:简单的读写操作(如
i++
)在底层可能被拆分为多条指令,多线程同时执行时导致结果错误。
什么是Java中的原子性、可见性和有序性?
- 原子性(Atomicity):保证操作不被中断(要么全执行,要么全不执行),对应解决 “操作被拆分导致的数据不一致” 问题,由
synchronized
、原子类等保证。 - 可见性(Visibility):保证变量修改被其他线程及时看到,对应解决 “工作内存与主内存同步延迟” 问题,由
volatile
、synchronized
等保证。 - 有序性(Ordering):保证指令执行顺序符合逻辑,对应解决 “指令重排序导致的多线程逻辑错误” 问题,由
volatile
、Happens-Before 规则等保证。
i++是原子操作吗?
i++
看似是一个简单的操作,但实际上包含三个独立的步骤:- 读取变量值:从主内存或缓存中读取
i
的当前值。 - 执行加 1 操作:在 CPU 中对读取的值进行加 1 计算。
- 写回新值:将计算结果写回主内存或缓存。
这三个步骤不是一个不可分割的整体,在多线程环境下可能被其他线程打断。
- 读取变量值:从主内存或缓存中读取
多线程下会出现错误
假设有两个线程同时执行
i++
,初始值i = 0
,期望结果:两个线程执行后,i
应该为2。但可能的执行顺序如下:- 线程 A 读取
i
的值为 0。 - 线程 B 读取
i
的值为 0(此时线程 A 还未写入新值)。 - 线程 A 执行加 1 操作,得到结果 1。
- 线程 B 执行加 1 操作,得到结果 1(因为 B 读取的是旧值 0)。
- 线程 A 将结果 1 写回主内存。
- 线程 B 将结果 1 写回主内存。
最终结果:
i
的值为 1,而不是预期的 2。- 线程 A 读取
解决方案
说说什么是指令重排
指令重排(Instruction Reordering) 是计算机系统中为提升执行效率而对指令执行顺序进行优化的技术。
为什么会发生指令重排?
指令重排的核心目的是提高程序运行效率。
- 编译器优化:Java 的 JIT(即时编译器)在编译字节码为机器码时,可能会调整指令顺序,比如将没有依赖关系的指令重新排序,减少 CPU 的等待时间。
- 处理器优化:现代 CPU 支持 “乱序执行”(Out-of-Order Execution),如果前一条指令需要等待内存读写(耗时操作),CPU 会先执行后面没有依赖关系的指令,充分利用 CPU 资源。
多线程下的问题:重排导致逻辑错误
多线程环境中,指令重排可能打破线程间的逻辑依赖,导致错误。因为重排只保证单线程结果正确,不考虑多线程间的交互。
最经典的例子是双重检查锁定(DCL)的单例模式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Singleton {
private static Singleton instance; // 未用volatile
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 可能被重排序
}
}
}
return instance;
}
}instance = new Singleton()
可拆分为三步:- 分配内存空间(memory = allocate ())
- 初始化对象(ctorInstance (memory))
- 把 instance 指向内存空间(instance = memory)
编译器可能重排序为 1→3→2。若线程 A 执行到 3(未执行 2)时,线程 B 进入第一次检查,发现
instance != null
,直接返回未初始化的对象,导致错误。给 instance 变量加上volatile
关键字,可以禁止指令重排。1
private static volatile Singleton instance; // 使用volatile
如何禁止指令重排?
volatile
关键字:通过插入内存屏障(禁止特定类型的重排序)阻止指令重排序。例如,对volatile
变量的写操作前插入 StoreStore 屏障,写操作后插入 StoreLoad 屏障,避免其前后的指令被重排序。- **
synchronized
/Lock
**:同一时间只有一个线程执行同步代码块,天然保证了代码块内的指令按顺序执行。 - happens-before 规则:JMM 定义的一套有序性规则,例如 “程序顺序规则”(单线程内指令按代码顺序执行)、“volatile 规则”(volatile 写先行发生于 volatile 读)、“锁规则”(解锁先行发生于加锁)等,通过这些规则保证多线程环境下的有序性。
as-if-serial
了解吗
单线程环境中,指令重排不会影响程序的最终结果,这是因为编译器和处理器遵循as-if-serial 语义—— 即 “不管怎么重排,单线程程序的执行结果都要和按代码顺序执行的结果一致”。
举个栗子:
1 | int a = 1; // 指令1 |
指令 1 和指令 2 没有依赖关系(互不影响结果),编译器可能先执行指令 2 再执行指令 1,但指令 3 必须在 1 和 2 之后(依赖它们的结果),因此最终c
的结果一定是 3,单线程下完全没问题。
说说对volatile
的理解
volatile
是 Java 中用于修饰变量的关键字,是一种轻量级的同步机制,主要用于解决多线程环境下变量的可见性和有序性问题,但不保证原子性。它的设计目标是提供比 synchronized
更低开销的同步方案,适用于特定场景。
volatile 的核心作用
保证可见性
可见性指:当一个线程修改了
volatile
变量的值后,其他线程能立即看到该变量的最新值。- 原理:
未被volatile
修饰的变量,线程会从 “工作内存”(CPU 缓存)读取和修改,修改后不会立即同步到 “主内存”;其他线程可能一直使用旧的工作内存值,导致数据不一致。
而volatile
变量的读写会强制通过 “主内存” 进行:- 写操作:线程修改
volatile
变量后,会立即将新值刷新到主内存。 - 读操作:线程读取
volatile
变量时,会先清空工作内存,再从主内存重新加载最新值。
- 写操作:线程修改
- 原理:
禁止指令重排
指令重排是编译器或 CPU 为优化性能,对代码执行顺序的调整(不影响单线程结果,但可能破坏多线程正确性)。
volatile
通过内存屏障(Memory Barrier)禁止指令重排,保证代码执行顺序与预期一致。- 原理:
JVM 会在volatile
变量的读写操作前后插入内存屏障,限制重排范围:- 禁止
volatile
变量写操作前的代码被重排到写操作之后; - 禁止
volatile
变量读操作后的代码被重排到读操作之前。
- 禁止
- 典型场景:单例模式的双重检查锁
- 原理:
不保证原子性
volatile
无法保证复合操作的原子性(即多线程下的 “读 - 改 - 写” 步骤可能被打断)。
volatile 的实现原理:内存屏障
JVM 通过插入内存屏障(特殊指令)保证 volatile
的可见性和有序性:
- StoreStore 屏障:在
volatile
写操作前插入,确保之前的普通写操作已刷新到主内存。 - StoreLoad 屏障:在
volatile
写操作后插入,防止写操作与后续的读操作重排。 - LoadLoad 屏障:在
volatile
读操作后插入,防止后续读操作与该读操作重排。 - LoadStore 屏障:在
volatile
读操作后插入,防止后续写操作与该读操作重排。
这些屏障强制 CPU 缓存与主内存同步,禁止指令重排,从而保证可见性和有序性。
volatile
与 synchronized
的区别
特性 | volatile | synchronized |
---|---|---|
原子性 | 不保证(仅可见性 / 有序性) | 保证(同步块内操作原子) |
可见性 | 保证 | 保证(解锁时刷新主内存) |
有序性 | 保证(禁止重排) | 保证(同步块内顺序执行) |
开销 | 轻量(无锁) | 较重(可能阻塞线程) |
适用场景 | 状态标记、禁止重排 | 复杂同步逻辑(需原子性) |
说说happens-before 规则
在 Java 内存模型(JMM)中,Happens-Before 规则是一组用于确定跨线程操作之间可见性的核心规则。它定义了 前一个操作的结果对后一个操作可见 的条件,是 JMM 保证多线程有序性和可见性的基础。
Happens-Before 的核心规则
程序顺序规则(Program Order Rule)
单线程内,每个操作Happens-Before后续的所有操作。
1
2
3int a = 1; // 操作1
int b = 2; // 操作2
int sum = a + b; // 操作3单线程下,操作 1 Happens-Before 操作 2,操作 2 Happens-Before 操作 3。但编译器可能重排操作 1 和 2 的顺序(只要不影响最终结果),因此多线程下不能依赖此规则。
监视器锁规则(Monitor Lock Rule)
对一个锁的解锁操作 Happens-Before 后续对同一锁的加锁操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class MonitorExample {
private int x = 0;
public void writer() {
synchronized (this) { // 加锁
x = 1; // 操作1
} // 解锁
} // 操作1 Happens-Before 后续其他线程的加锁
public void reader() {
synchronized (this) { // 加锁(必须等待前面的解锁)
int y = x; // 操作2
} // 解锁
}
}线程 A 解锁后,线程 B 加锁时能看到 A 对 x 的修改(x=1)。
volatile 变量规则(Volatile Variable Rule)
对一个
volatile
变量的写操作 Happens-Before 后续对同一变量的读操作。1
2
3
4
5
6
7
8
9
10
11
12
13private volatile boolean flag = false;
// 线程A
public void writer() {
flag = true; // 写操作
} // 写操作Happens-Before 线程B的读操作
// 线程B
public void reader() {
while (!flag) { // 读操作(保证看到最新值)
// 循环等待
}
}volatile
通过内存屏障禁止重排,确保写操作的结果对读操作可见。线程启动规则(Thread Start Rule)
线程的
start()
方法 Happens-Before 此线程的任意操作。1
2
3
4
5
6
7Thread t = new Thread(() -> {
// 线程t的操作
System.out.println("线程t启动"); // 操作1
});
t.start(); // 主线程调用start()
// start() Happens-Before 操作1主线程调用
start()
后,线程 t 的操作能看到主线程在start()
前的所有修改。线程终止规则(Thread Termination Rule)
线程的所有操作 Happens-Before 其他线程检测到该线程已终止(如通过
Thread.join()
返回、Thread.isAlive()
返回false
)。1
2
3
4
5
6
7
8
9Thread t = new Thread(() -> {
// 线程t的操作
System.out.println("线程t即将结束"); // 操作1
});
t.start();
t.join(); // 主线程等待t终止
// 操作1 Happens-Before t.join()返回
System.out.println("线程t已结束");主线程能看到线程 t 的所有操作结果。
中断规则(Interruption Rule)
线程 A 调用
threadB.interrupt()
Happens-Before 线程 B 检测到中断(如通过Thread.interrupted()
或Thread.isInterrupted()
)。1
2
3
4
5
6
7
8
9
10Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 循环
}
System.out.println("线程t被中断"); // 操作1
});
t.start();
t.interrupt(); // 主线程中断t
// interrupt() Happens-Before 操作1线程 t 能看到主线程调用
interrupt()
的结果。对象终结规则(Finalizer Rule)
对象的初始化完成(构造函数执行结束) Happens-Before 它的
finalize()
方法开始。1
2
3
4
5
6
7
8
9
10
11
12
13public class Resource {
public Resource() {
// 初始化操作
System.out.println("Resource初始化完成"); // 操作1
}
@Override
protected void finalize() throws Throwable {
// 对象销毁前的清理
System.out.println("Resource finalize()开始"); // 操作2
}
}
// 操作1 Happens-Before 操作2确保对象初始化完成后才会执行
finalize()
。传递性规则 (Transitivity)
如果操作 A Happens-Before 操作 B,操作 B Happens-Before 操作 C,则操作 A Happens-Before 操作 C。
1
2
3
4
5
6
7
8
9
10
11
12
13// 线程A
public void writer() {
a = 1; // 操作1
flag = true; // 操作2(volatile写)
}
// 线程B
public void reader() {
while (!flag) { // 操作3(volatile读)
// 循环
}
int b = a; // 操作4
}根据规则:
- 操作 1 Happens-Before 操作 2(程序顺序规则)。
- 操作 2 Happens-Before 操作 3(volatile 变量规则)。
- 操作 3 Happens-Before 操作 4(程序顺序规则)。
通过传递性,操作 1 Happens-Before 操作 4,因此线程 B 能看到线程 A 对a
的修改(a=1
)。
锁
什么是乐观锁和悲观锁
在并发编程中,乐观锁和悲观锁是两种不同的锁策略,用于解决多线程环境下对共享资源的访问冲突。它们的核心区别在于对数据冲突的预期态度和处理方式。
特性 | 悲观锁 | 乐观锁 |
---|---|---|
核心假设 | 假设冲突概率高 | 假设冲突概率低 |
加锁时机 | 先加锁,再操作 | 先操作,后验证 |
实现方式 | synchronized 、ReentrantLock 、数据库行锁(SELECT … FOR UPDATE) |
CAS、版本号机制 |
性能 | 写操作多时性能较差(锁竞争开销大) | 读操作多时性能较好(无锁开销) |
冲突处理 | 线程阻塞等待 | 重试或放弃操作 |
典型场景 | 库存扣减、金融交易 | 缓存更新、统计数据 |
什么是Java的CAS
?
在 Java 里,CAS
即Compare-And-Swap(比较并交换),它是一种无锁的原子操作,作用是实现多线程环境下的变量同步。CAS
操作主要依赖于底层硬件(像 CPU 指令)来保证操作的原子性,避免了使用传统锁(例如synchronized
)带来的线程阻塞和上下文切换开销。
CAS 操作包含三个参数:
- V:要更新的变量(var)
- E:预期值(expected)
- N:新值(new)
如果变量 V 的值等于 E ,则将 V 的值变为 N;如果不相等则说明有其他线程在操作,放弃更新。整个过程由 CPU 保证原子性。
Java 中的实现
Java 通过Unsafe
类和java.util.concurrent.atomic
包提供 CAS 支持。
①
Unsafe
类(底层实现)Unsafe
类提供了硬件级别的原子操作,例如:1
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
o
:需要修改的对象。offset
:对象中变量的内存偏移量。expected
:预期值。x
:新值。
② 原子类(Atomic Classes)
基于
Unsafe
封装的原子类,如AtomicInteger
、AtomicLong
等:1
2
3
4
5
6
7
8
9
10import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
private static AtomicInteger counter = new AtomicInteger(0);
public static void increment() {
// 自增操作 = getAndAdd(1) = CAS循环
counter.incrementAndGet();
}
}
优点:
- 无锁性能:避免了线程阻塞和上下文切换,在竞争不激烈时性能远高于
synchronized
。 - 原子性保障:硬件级别的原子操作,无需依赖操作系统的锁机制。
CAS
会有什么问题
ABA 问题
若变量 V 的值从 A 变为 B,再变回 A,CAS 会认为值未变化,但实际已被修改。
解决方法:使用AtomicStampedReference
(带版本号的原子引用)。他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。自旋开销
竞争激烈时,线程频繁重试 CAS 会消耗大量 CPU 资源。
解决方法:限制重试次数或使用锁(如ReentrantLock
)。只能保证单个变量的原子性
CAS 只能对单个变量操作,如需原子操作多个变量,需将它们封装成一个对象(使用
AtomicReference
)。
什么是Java的 AQS
?
AQS 是 Java 并发编程的基础框架,它通过一个volatile int
类型的state
变量表示锁状态,并用 CLH 队列管理等待线程。例如,ReentrantLock
用state
记录重入次数,Semaphore
用state
表示可用许可数。子类需要通过 CAS 操作修改state
,并实现tryAcquire
和tryRelease
方法来控制锁的获取与释放。
在 JUC 中的典型应用
- **
ReentrantLock
**:公平锁和非公平锁的差异在于入队策略。非公平锁会先尝试 CAS 获取锁,失败后才入队;公平锁则直接入队等待。 - **
Semaphore
**:当线程调用acquire()
时,若state
大于 0,则获取许可并减少state
;否则阻塞。release()
增加state
并唤醒等待线程。 - **
CountDownLatch
**:初始化时state
为指定计数,线程调用countDown()
时通过 CAS 减少state
,当state
减为 0 时,唤醒所有等待线程。 - **
ReentrantReadWriteLock
**:使用两个 AQS 子类分别管理读锁和写锁,支持多读单写。
优缺点
- 优点
- 代码复用:避免重复实现队列管理和线程阻塞 / 唤醒逻辑。
- 灵活性:通过模板方法支持多种锁语义(如公平锁、读写锁)。
- 高性能:CAS 和 CLH 队列在无竞争时效率高。
- 缺点
- 实现复杂:需要深入理解 AQS 的模板方法和状态管理。
- 只能支持独占或共享模式中的一种(子类需选择其一)。
AQS
的核心原理
AQS 的核心是通过state
变量和 CLH 队列实现线程同步。当线程竞争锁时:
- 获取锁:线程调用
acquire()
方法,尝试通过 CAS 将state
从 0 改为 1。若成功,则获取锁;否则放入队列。 - 入队等待:线程被封装成
Node
,通过 CAS 插入队尾,并通过LockSupport.park()
阻塞。 - 释放锁:持有锁的线程调用
release()
方法,将state
置为 0,并通过LockSupport.unpark()
唤醒后继节点。
AQS 支持两种同步方式:
独占模式:如
ReentrantLock
,同一时间只有一个线程能持有锁,state
表示重入次数。获取锁时,tryAcquire
需判断state
是否为 0(未锁定)或当前线程是否已持有锁(可重入)。核心方法:
acquire
:获取锁,失败进入等待队列release
:释放锁,唤醒等待队列中的线程
共享模式:如
Semaphore 和 CountDownLatch
,state
表示可用许可数。调用acquireShared
时,若state
大于 0,则 CAS 减少state
并获取许可;否则入队等待。释放时,releaseShared
增加state
并唤醒后续节点。核心方法:
acquireShared
:共享模式获取锁releaseShared
:共享模式释放锁
AQS 如何处理线程中断和超时?
- 线程中断:
acquireInterruptibly()
:可中断获取锁,被中断时抛出InterruptedException
。lockInterruptibly()
:如ReentrantLock
的可中断锁实现。
- 超时机制:
tryAcquireNanos()
:指定时间内尝试获取锁,超时则返回false
。
说说Java中ReentrantLock
的实现原理?
在 Java 中,
ReentrantLock
是一个可重入的互斥锁,它位于java.util.concurrent.locks
包下,实现了Lock
接口,功能类似于synchronized
关键字,但更加灵活。例如:- 可重入:线程可多次调用
lock()
而不阻塞自己(需对应次数的unlock()
)。 - 公平锁:按请求顺序获取锁,避免 “线程饥饿”(但性能开销大)。
- 可中断锁:通过
lockInterruptibly()
允许线程在等待锁时响应中断。
- 可重入:线程可多次调用
ReentrantLock
的实现基于AQS(AbstractQueuedSynchronizer) 和CAS(Compare-And-Swap) 操作:- AQS:作为同步器的基础框架,线程队列和管理锁状态(
state
)记录锁的持有次数。state = 0
:锁未被持有。state > 0
:锁被持有,数值表示当前线程的重入次数。
线程通过 CAS 操作尝试修改state
,成功则获取锁,失败则进入 AQS 队列等待。
- CAS:用于原子性地更新锁状态,避免使用传统锁的开销。
- AQS:作为同步器的基础框架,线程队列和管理锁状态(
公平锁 vs 非公平锁(如何实现?)
特性 公平锁 非公平锁 获取顺序 遵循请求顺序,先到先得(FIFO)。 允许插队,新请求可能比等待中的线程优先获取锁。
是随机或者按照其他优先级排序调度开销 较高,因为需要维护严格的队列顺序。 较低,因为减少了线程切换的开销。 吞吐量 可能较低,尤其在锁持有时间短的场景。 通常较高,因为减少了线程挂起和唤醒的次数。 饥饿可能性 几乎不会出现,每个线程都能按序获得锁。 存在可能,等待中的线程可能长时间无法获取锁。 - 公平锁:适用于需要保证线程执行顺序的场景,比如资源分配、定时任务执行等。
- 非公平锁:适合对吞吐量要求高、锁持有时间短的场景,像大多数的 RPC 调用、缓存更新操作等。
为什么默认是非公平锁?
非公平锁性能更高,因为省去了检查队列的开销。大多数场景下,线程的执行顺序对结果无影响,此时非公平锁的吞吐量更好。
可重入性实现
ReentrantLock
通过 AQS 的state
变量记录锁的重入次数:当
state
为 0 时,表示锁未被任何线程持有。当
state
大于 0 时,表示锁已被持有,且数值等于当前线程的重入次数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 锁未被持有,尝试获取
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 当前线程已持有锁,增加重入次数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
锁释放流程
释放锁时,每次调用
unlock()
会减少state
的值:- 当
state
减为 0 时,表示锁完全释放,唤醒队列中的下一个线程。 - 如果
state
仍大于 0,表示锁仍被当前线程持有(重入未完全释放)。
1
2
3
4
5
6
7
8
9
10
11
12protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}- 当
说说Java中synchronized
的实现原理
在 Java 里,synchronized
关键字的主要作用是实现线程同步,保证同一时刻只有一个线程能够访问被它修饰的代码块或者方法。
synchronized
是基于Java
对象头**(Object Header)
**和 **Monitor
**(监视器,也称为管程)实现的线程同步机制。使用的时候不用手动去 lock 和 unlock,JVM 会自动加锁和解锁。
- 对象头(Mark Word):在对象的头部,有一部分空间用于存储锁的状态信息,比如偏向锁、轻量级锁、重量级锁这些标记。
- Monitor(监视器):这是 Java 对象所拥有的一个内置锁,其实现依赖于底层操作系统的互斥量(Mutex)。
Monitor
(监视器)机制Monitor
是 Java 中实现同步的基础,每个对象在内存中都有一个对象头——Mark Word,用于存储锁的状态,以及Monitor
对象的指针。当一个线程尝试访问被synchronized
修饰的代码块或方法时:- 获取锁:线程必须先获得对象的
Monitor
。如果Monitor
已被其他线程持有,则当前线程会被阻塞,进入Monitor
的等待队列。 - 释放锁:持有
Monitor
的线程执行完同步代码后释放锁,唤醒等待队列中的其他线程竞争锁。
底层实现:
Monitor
依赖于操作系统的互斥量(Mutex)实现,这涉及用户态与内核态的切换,因此重量级锁的性能开销较大。在 Hotspot 虚拟机中,
Monitor
由ObjectMonitor
实现,其核心结构如下:1
2
3
4
5
6
7
8
9
10ObjectMonitor {
_header = NULL; // Mark Word 副本
_count = 0; // 重入次数
_waiters = 0; // 等待线程数
_recursions = 0; // 锁重入次数
_owner = NULL; // 持有锁的线程
_WaitSet = NULL; // 等待队列(wait() 的线程)
_cxq = NULL; // 竞争队列
_EntryList = NULL; // 阻塞队列
}- 获取锁:线程必须先获得对象的
对象头
(Object Header)
:锁的存储基础Java 对象在内存中分为三部分:
- 对象头(Header):存储锁状态、GC 分代年龄、哈希码等。
- 实例数据(Instance Data):对象的字段值。
- 对齐填充(Padding):补齐内存对齐。
其中,对象头中的 Mark Word 存储了锁的状态信息。
Mark Word 结构:
Mark Word
是一个动态数据结构,根据锁状态不同存储不同信息。以 64 位 JVM 为例:- 无锁状态:
Mark Word
存储对象哈希码、CG分代信息。 - 偏向锁:
Mark Word
存储偏向线程 ID、时间戳等。 - 轻量级锁:
Mark Word
存储线程栈帧中的锁记录指针。 - 重量级锁:
Mark Word
存储指向 Monitor 对象的指针。
- 无锁状态:
锁升级过程
JDK 6 之后引入锁升级机制,避免直接使用重量级锁,优化了性能:
① 偏向锁(Biased Locking)
适用场景:
只有一个线程访问同步块。
原理:
当第一个线程获取锁时,会在 Mark Word 中记录该线程的 ID(偏向锁状态)。此后该线程再次进入同步块时,无需任何同步操作,直接获取锁。优点:
无竞争下几乎无开销。
升级触发:
当其他线程尝试竞争该锁时,偏向锁升级为轻量级锁。
② 轻量级锁(Lightweight Locking)
适用场景:
多个线程交替访问同步块(无竞争)。
原理:
- 线程在进入同步块前,会在栈帧中创建用于存储锁记录(Lock Record)的空间。
- 然后通过 CAS(Compare-and-Swap)操作,尝试将对象头的 Mark Word 复制到锁记录中,并将对象头指向锁记录的地址。
- 如果 CAS 操作成功,线程就获取到了锁;若失败,则表示有其他线程正在持有锁,当前线程会进行自旋等待,避免上下文切换。
升级触发:
当多个线程同时竞争锁时(CAS 失败多次),轻量级锁升级为重量级锁。
③ 重量级锁(Heavyweight Lock)
适用场景:
多个线程同时竞争锁。
原理:
Mark Word 指向操作系统的 Monitor 对象,竞争失败的线程会被阻塞(进入内核态),锁释放后需唤醒线程(从内核态恢复)。缺点:
线程阻塞和唤醒的开销大。
总结
synchronized
的核心原理是通过对象头的 Mark Word 标记锁状态,并利用 Monitor 实现线程同步。锁升级机制(偏向锁 → 轻量级锁 → 重量级锁)在不同竞争程度下平衡了性能和安全性,使得它在大多数场景下成为首选的同步方式。
说说synchronized
和ReentrantLock
的区别
特性 | synchronized | ReentrantLock |
---|---|---|
实现方式 | JVM 内置关键字 | JUC 包下的类 |
锁的释放 | 自动释放 | 手动释放 |
公平性 | 非公平锁 | 支持公平 / 非公平锁 |
可中断性 | 不可中断 | 可中断 |
超时机制 | 不支持 | 支持 |
锁状态判断 | 不支持 | 支持 |
性能 | 优化后接近 ReentrantLock | 竞争激烈时更灵活 |
- 优先使用
synchronized
,因为它简洁易用,且 JVM 对其进行了优化。 - 当需要高级锁特性(如可中断锁、公平锁、超时机制)时,选择
ReentrantLock
。
什么是线程死锁
两个或多个线程在执行过程中,由于互相持有对方需要的资源(通常是锁),并无限期地等待对方释放资源,从而导致所有线程都无法继续执行下去的一种永久性阻塞状态。
死锁发生的条件是什么?如何解决?
死锁产生的 4 个必要条件(缺一不可)
- 互斥条件:资源(如锁)具有排他性,同一时间只能被一个线程持有(例如
synchronized
锁、ReentrantLock
都是互斥的)。 - 持有并等待条件:线程持有至少一个资源,同时又在等待获取其他线程持有的资源。
- 不可剥夺条件:线程已持有的资源不能被其他线程强制剥夺,只能由线程主动释放(例如锁只能由持有者主动释放)。
- 循环等待条件:多个线程形成闭环等待链,每个线程都在等待下一个线程持有的资源(例如线程 1 等线程 2 的资源,线程 2 等线程 1 的资源)。
- 互斥条件:资源(如锁)具有排他性,同一时间只能被一个线程持有(例如
如何避免死锁?
破坏 “循环等待” 条件
给所有资源定义统一的获取顺序,所有线程必须按顺序获取资源。例如:规定 “必须先获取 LOCK_A,再获取 LOCK_B”,避免交叉获取。
破坏 “持有并等待” 条件
线程获取资源时,一次性获取所有所需资源(若获取不全则释放已持有的资源,重新尝试)。
破坏 “不可剥夺” 条件
使用可中断的锁(如
ReentrantLock
的tryLock(timeout)
),若超时未获取到锁,则主动释放已持有的资源。减少锁的持有时间
同步块只包含必要的代码,避免长时间持有锁,降低交叉等待的概率。
如何检测死锁?
在 Linux 生产环境中
可以先使用
top
ps
等命令查看进程状态,看看是否有进程占用了过多的资源。使用 JDK 自带工具(生产环境常用)
使用
jps -l
查看当前进程,然后使用jstack 进程号
查看当前进程的线程堆栈信息,看看是否有线程在等待锁资源。图形化工具:
jconsole
或VisualVM
可视化工具,查看线程状态和死锁。
并发工具类
什么是Java的CountDownLatch
?
CountDownLatch
是 Java 并发包(java.util.concurrent
)中的一个同步工具类,用于让一个或多个线程等待其他线程完成操作后再继续执行。它通过一个计数器实现,初始时设置计数器值,线程完成操作后调用 countDown()
方法递减计数器,等待线程通过 await()
方法阻塞,直到计数器值为 0。
1 | int workerCount = 3; |
什么是Java的CyclicBarrier
?
CyclicBarrier
是 Java 并发包(java.util.concurrent
)中的一个同步工具类,用于让一组线程在到达某个屏障点(Barrier)时相互等待,直到所有线程都到达该点后,再继续执行后续操作。与 CountDownLatch
不同,CyclicBarrier
的计数器可以循环使用(重置后可再次等待),因此适用于需要多轮协作的场景。
1 | // 创建一个屏障,等待3个线程 |
CountDownLatch
和CyclicBarrier
的区别
特性 | CountDownLatch | CyclicBarrier |
---|---|---|
使用次数 | 计数器减到 0 后无法重置,只能用一次。 | 支持循环使用(通过 reset() 方法)。 |
计数方向 | 递减(countDown) | 递增(await) |
等待方向 | 主线程等待子线程 | 线程互相等待 |
重置功能 | ❌ 不可重置 | ✅ 可重置 |
回调功能 | ❌ 无 | ✅ 全员到齐后执行指定任务 |
适用场景 | 启动前初始化,结束前收尾 | 计算任务拆分,所有线程都到达后才能继续 |
异常处理 | 计数不受影响,需要处理 InterruptedException 。 |
需要处理 BrokenBarrierException (如某个线程中断导致屏障破坏)。 |
- CyclicBarrier:像团队爬山,所有人在每个山顶等待,到齐后一起出发下一段路,可重复使用。
- CountDownLatch:像火箭发射倒计时,各部门完成准备后计数减 1,倒计时结束后发射(且只发射一次)。
什么是Java的Semaphore
?
Semaphore
(信号量)是 Java 并发包(java.util.concurrent
)中的一个同步工具类,用于控制同时访问某个资源的线程数量。它通过维护一个许可证(permit)计数器,线程在访问资源前必须先获取许可证,使用完后释放许可证,从而限制并发访问的线程上限。
1 | Semaphore semaphore = new Semaphore(3); |
什么是Java的Exchanger
?
Exchanger
是 Java 并发包(java.util.concurrent
)中的一个同步工具类,用于两个线程之间交换数据。它允许两个线程在某个同步点互相交换各自持有的数据,当一个线程到达 exchange()
方法时,会阻塞直到另一个线程也到达该方法,然后两者交换数据并继续执行。
1 | import java.util.concurrent.Exchanger; |
线程池
什么是线程池
线程池是一种 线程复用机制,通过预先创建/管理线程集合,避免频繁线程创建销毁带来的性能开销,实现任务的并发执行。
如何创建线程池
手动创建
ThreadPoolExecutor
(推荐,更安全可控)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy()
);
try {
for (int i = 0; i < 9; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"=>执行业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}通过
Executors
工厂类创建(简单快捷)1
2
3
4
5
6
7
8
9
10
11
12ExecutorService threadPool = Executors.newFixedThreadPool(5);//创建一个固定大小的线程池
try {
for (int i = 0; i < 4; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"=>执行业务");
});
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
threadPool.shutdown();
}
线程池的原理
graph TD
A[提交任务] --> B(核心线程<br>是否空闲?):::decision
B -->|是| C[立即执行]:::process
B -->|否| D(任务队列<br>是否已满?):::decision
D -->|否| E[加入队列等待]:::process
D -->|是| F(线程数<br><最大线程数?):::decision
F -->|是| G[创建临时线程执行]:::process
F -->|否| H[触发拒绝策略]:::process
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
线程池常见的参数有哪些?
1 | public ThreadPoolExecutor(int corePoolSize, |
corePoolSize
(核心线程数)- 作用:线程池长期保持的最小线程数(即使空闲也不会被销毁,除非开启
allowCoreThreadTimeOut
)。 - 配置建议:
- CPU 密集型任务(如计算):设为 CPU 核心数 + 1(减少线程切换开销)。
- IO 密集型任务(如网络 / DB 操作):设为 2 × CPU 核心数(利用等待时间并行处理)。
- 混合型任务:
(线程等待时间/线程CPU时间 + 1) * CPU核数
- 作用:线程池长期保持的最小线程数(即使空闲也不会被销毁,除非开启
maximumPoolSize
(最大线程数)- 作用:线程池允许创建的最大线程数量(包括核心线程和临时线程)
- 关键规则:
- 当
任务数 > corePoolSize
且工作队列已满
时,才会创建临时线程 - 临时线程数量 =
maximumPoolSize - corePoolSize
- 当
- 配置建议:通常设为
corePoolSize * 2
,但需考虑系统资源上限
keepAliveTime
(空闲线程存活时间)- 作用:临时线程空闲时的存活时间,超时后会被回收,需配合
TimeUnit
参数使用 - 默认值:
60
秒。
- 作用:临时线程空闲时的存活时间,超时后会被回收,需配合
unit
(时间单位)作用:指定
keepAliveTime
的时间单位常用值:
1
2
3TimeUnit.SECONDS // 秒
TimeUnit.MILLISECONDS // 毫秒
TimeUnit.MINUTES // 分钟
workQueue
(任务队列)作用:缓存待执行任务的队列,核心线程忙时任务进入队列等待。
常见实现类:
队列类型 容量特性 适用场景 线程池默认使用 ArrayBlockingQueue
有界 任务量可控、需限制内存 手动配置(如自定义线程池) LinkedBlockingQueue
无界(默认) 任务处理快、需避免任务被拒 FixedThreadPool
、SingleThreadExecutor
SynchronousQueue
容量 0 任务处理极快、需动态线程数 CachedThreadPool
PriorityBlockingQueue
无界 任务需优先级排序 需手动配置(如自定义线程池) DelayQueue
无界 定时任务、延迟执行 ScheduledThreadPool
threadFactory
(线程工厂)- 作用:自定义线程创建逻辑(如命名线程、设置优先级、守护线程等)。
- 默认值:
Executors.defaultThreadFactory()
(创建的线程名为pool-1-thread-1
格式)。 - 场景:建议自定义工厂(如
new ThreadFactory() { ... }
),便于日志追踪和问题排查。
handler
(拒绝策略)作用:当线程数=maxPoolSize且队列已满时,对新任务的拒绝策略。
默认值:
AbortPolicy
(抛异常RejectedExecutionException
)。常见策略:
策略类 行为 AbortPolicy
(默认)抛出 RejectedExecutionException
异常CallerRunsPolicy
由提交任务的线程直接执行该任务(同步执行) DiscardPolicy
静默丢弃新任务,不抛异常 DiscardOldestPolicy
丢弃队列中最旧的任务,然后重试提交
线程池的常用的阻塞队列有哪些?
**
ArrayBlockingQueue
**(有界阻塞队列)- 底层由数组实现,容量一旦创建,就不能修改。
- 适用场景:需要严格控制队列长度,防止资源耗尽(如高并发限流)。
- 在线程池中: 当队列满时,提交新任务会触发线程池的拒绝策略。
**
LinkedBlockingQueue
**(默认无界,也可指定容量)- 底层由链表实现,默认大小为
Integer.MAX_VALUE
(无界易引发OOM),可指定为有界队列 - 适用场景:
- 有界: 需要控制任务数量,但任务量波动较大。
- 无界: 任务到达速度通常不会长时间远高于处理速度,且能容忍队列暂时增长(需警惕 OOM)。
newFixedThreadPool
和newSingleThreadExecutor
默认使用无界LinkedBlockingQueue
。
- 在线程池中:
- 有界: 队列满时触发拒绝策略。
- 无界: 理论上队列永远不会满(除非 OOM),因此提交任务通常不会触发拒绝策略(除非线程池关闭或饱和策略有其他限制)。
- 底层由链表实现,默认大小为
**
SynchronousQueue
**(同步队列)- 没有容量的阻塞队列是一种没有容量的阻塞队列,生产者插入任务时必须等待消费者取出。它直接传递任务,避免任务排队,适合快速响应的场景,但对线程池的扩缩容更敏感。
- 任务处理速度快:适合需要快速响应的场景。
- 创建线程无限制:如**
Executors.newCachedThreadPool()
**默认使用此队列,配合Integer.MAX_VALUE
最大线程数。 - 在线程池中:
- 如果没有空闲工作线程且当前线程数未达核心线程数,会创建新线程。
- 如果没有空闲工作线程且当前线程数已达到核心线程数(但小于最大线程数),会创建新线程。
- 如果没有空闲工作线程且当前线程数已达最大线程数,则提交任务会立即触发拒绝策略(因为队列无法存放任何任务)。这使得
SynchronousQueue
能非常快速地响应系统过载。
**
DelayedWorkQueue
**(延迟队列)实现: 基于
PriorityQueue
实现的延时近似无界阻塞队列。特性:
延时获取: 元素必须实现
Delayed
接口。只有在其指定的延迟时间到期后,元素才能被取出。- 近似无界: 容量自动增长。
无界风险: OOM 风险。
适用场景: 需要延迟执行任务的场景,如定时任务调度、缓存过期失效等。
在线程池中:
- 通常需要配合自定义的线程池使用(如
ScheduledThreadPoolExecutor
内部使用了类似DelayQueue
的机制)。提交任务时,即使队列未满,任务也不会立即被执行,而是等到其延迟到期。队列满不是主要问题(近似无界),但需注意 OOM。
- 通常需要配合自定义的线程池使用(如
**
PriorityBlockingQueue
**(支持优先级排序)- 实现: 支持优先级排序的近似无界阻塞队列。
- 特性:
- 优先级: 元素必须实现
Comparable
接口,或者在构造队列时提供Comparator
。队列按照优先级顺序出队(优先级最高的先出队)。 - 近似无界: 容量自动增长(受限于内存),所以
put
操作通常不会阻塞(除非资源耗尽)。 - 无界风险: 同样有 OOM 风险。
- 非 FIFO: 出队顺序由优先级决定。
- 优先级: 元素必须实现
- 适用场景: 需要根据任务优先级而非提交顺序来执行任务的场景。
- 在线程池中: 由于近似无界,提交任务通常不会因队列满而触发拒绝策略(优先级低的可能会长时间等待)。
线程池的拒绝策略有哪些?
当线程池无法接受新的任务时,也就是线程数达到 maximumPoolSize,任务队列也满了的时候,就会触发拒绝策略。
AbortPolicy
(默认):直接抛出RejectedExecutionException
异常,拒绝新任务。CallerRunsPolicy
:由提交任务的线程(如主线程)直接执行被拒任务。DiscardPolicy
:静默丢弃被拒任务,无异常、无日志。DiscardOldestPolicy
:移除队列中最旧的任务(队列头部),重试提交新任务。自定义拒绝策略:默认策略不能满足需求,可以通过实现 RejectedExecutionHandler 接口来定义自己的淘汰策略。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public class MyRejectedExecution implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("Task " + r.toString() + " rejected. Queue size: " + executor.getQueue().size());
}
}
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new MyRejectedExecution()
);
try {
for (int i = 0; i < 9; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"=>办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
如何选择拒绝策略
需要结合队列来选择
- 有界队列:常用
AbortPolicy
(抛异常)或CallerRunsPolicy
(让提交线程执行任务)。 - 无界队列:理论上不会触发拒绝策略(但需防OOM)。
SynchronousQueue
:建议用DiscardPolicy
或自定义策略避免阻塞提交线程。
有哪几种常见的线程池
- FixedThreadPool(固定线程数线程池)
- 特点:
核心线程数 = 最大线程数(通过Executors.newFixedThreadPool(n)
的n
指定),线程池中的线程数量固定不变。
使用无界阻塞队列LinkedBlockingQueue
存储等待任务。
线程空闲时不会被回收(核心线程数 = 最大线程数,且无超时机制)。 - 适用场景:
适用于任务数量已知、任务执行时间较长且稳定的场景(如服务器接收固定数量的并发请求)。
- 特点:
- CachedThreadPool(可缓存线程池)
- 特点:
核心线程数 = 0,最大线程数 =Integer.MAX_VALUE
(理论上无限)。
使用**同步队列SynchronousQueue
**(不存储任务,直接传递给线程)。
线程空闲 60 秒后会被自动回收,因此空闲时几乎不占用资源。 - 适用场景:
适用于任务数量多但执行时间短、突发且临时的任务(如临时批量处理短任务)。
缺点:最大线程数过大,若任务执行时间过长,可能创建大量线程导致资源耗尽。
- 特点:
- SingleThreadExecutor(单线程线程池)
- 特点:
核心线程数 = 1,最大线程数 = 1,仅用一个线程执行所有任务。
使用**无界阻塞队列LinkedBlockingQueue
**,任务按提交顺序串行执行。
若唯一线程因异常终止,会自动创建新线程替代。 - 适用场景:
适用于需要任务串行执行(如日志写入、顺序处理数据)或避免并发冲突的场景。
- 特点:
- ScheduledThreadPool(定时任务线程池)
- 特点:
核心线程数固定(通过Executors.newScheduledThreadPool(n)
的n
指定),最大线程数 =Integer.MAX_VALUE
。
使用**延迟队列DelayedWorkQueue
**(支持定时 / 周期性执行)。
支持定时(schedule()
)或周期性(scheduleAtFixedRate()
)执行任务。 - 适用场景:
适用于需要定时执行(如延迟 3 秒后执行)或周期性执行(如每隔 1 分钟执行一次)的任务(如定时备份、心跳检测)。
- 特点:
为什么不建议使用Executors去创建线程池
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1)
newFixedThreadPool(n)
和newSingleThreadExecutor()
:队列无界,可能导致 OOM这两种线程池的核心线程数 = 最大线程数(固定大小),工作队列使用
LinkedBlockingQueue
,而该队列的默认容量是Integer.MAX_VALUE
(约 20 亿)。当任务提交速度远快于线程处理速度时,任务会在队列中无限堆积,占用大量内存,最终导致 OOM(内存溢出)。2)
newCachedThreadPool()
:线程数无界,可能导致资源耗尽该线程池的核心线程数为 0,最大线程数为
Integer.MAX_VALUE
,线程空闲 60 秒后销毁,工作队列使用SynchronousQueue
(不存储任务,直接提交给线程)。当任务提交量激增时,线程池会无限创建新线程(理论上可达 20 亿)。过多的线程会导致:
- 大量 CPU 时间用于线程上下文切换,系统吞吐量骤降;
- 线程占用的内存(如栈空间)急剧增加,最终触发 OOM。
3)
newScheduledThreadPool(n)
:存在线程数或队列的潜在问题本质是支持定时任务的线程池,其核心线程数固定,但工作队列类似
DelayQueue
,若任务调度不当(如大量长期未执行的定时任务),也可能导致队列堆积和资源耗尽。
线程池的生命周期
RUNNING(运行中)
初始状态:线程池创建后默认进入此状态。
行为:
- 能接受新任务(
execute()
方法正常提交)。 - 能处理阻塞队列中已存在的任务。
- 能接受新任务(
SHUTDOWN(关闭中)
触发条件:调用
shutdown()
方法。行为:
- 不接受新任务(新提交的任务会被拒绝策略处理)。
- 继续处理阻塞队列中已存在的任务,直到队列清空。
转换:当队列中所有任务处理完毕,且工作线程数量为 0 时,进入
TIDYING
状态。
STOP(停止中)
触发条件:调用
shutdownNow()
方法。行为:
- 不接受新任务。
- 立即中断正在执行的任务(通过
Thread.interrupt()
)。 - 清空阻塞队列(未执行的任务被移除并返回)。
转换:当所有工作线程都已中断(数量为 0)时,进入
TIDYING
状态。
TIDYING(整理中)
触发条件:
- 从
SHUTDOWN
转换:队列清空 + 工作线程数量为 0。 - 从
STOP
转换:工作线程数量为 0。
- 从
行为:
- 线程池进入 “整理” 状态,此时会执行
terminated()
钩子方法(默认空实现,可重写用于资源清理)。
- 线程池进入 “整理” 状态,此时会执行
TERMINATED(已终止)
- 触发条件:
terminated()
方法执行完毕。 - 行为:线程池生命周期结束,无法再执行任何任务。
- 触发条件:
方法 | 作用 | 状态转换触发 |
---|---|---|
shutdown() |
平缓关闭:不接受新任务,处理完队列任务 | RUNNING → SHUTDOWN |
shutdownNow() |
强制关闭:中断任务,清空队列 | RUNNING → STOP |
isShutdown() |
判断是否处于 SHUTDOWN /STOP 状态 |
- |
isTerminated() |
判断是否处于 TERMINATED 状态 |
- |
awaitTermination() |
阻塞等待线程池进入 TERMINATED 状态 |
- |
线程池中shutdown (),shutdownNow()有什么区别
方法 | 任务处理 | 线程状态 | 响应中断 |
---|---|---|---|
shutdown() |
已提交的任务会继续执行(包括队列中等待的任务) | 不再接受新任务,线程池状态变为 SHUTDOWN |
不中断正在执行的线程 |
shutdownNow() |
尝试终止所有活跃线程,未执行的任务返回 | 立即终止,线程池状态变为 STOP |
中断正在执行的线程(通过 Thread.interrupt() ) |






- 1. Java 并发面试题
- 1.1. 基础
- 1.2. ThreadLocal
- 1.3. Java内存模型
- 1.4. 锁
- 1.5. 并发工具类
- 1.6. 线程池