Java 并发编程
Java 并发编程 #
高频面试核心:线程、锁、线程池、并发工具类
📋 目录 #
线程基础 #
线程创建方式 #
// 1. 继承Thread
class MyThread extends Thread {
@Override
public void run() {}
}
// 2. 实现Runnable
new Thread(() -> {}).start();
// 3. 实现Callable(有返回值)
FutureTask<String> future = new FutureTask<>(() -> "result");
new Thread(future).start();
String result = future.get();
// 4. 线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {});
线程状态转换 #
| 状态 | 说明 | 触发条件 |
|---|---|---|
| NEW | 新建 | new Thread() |
| RUNNABLE | 可运行 | start() |
| BLOCKED | 阻塞 | 等待synchronized锁 |
| WAITING | 等待 | wait()/join()/park() |
| TIMED_WAITING | 计时等待 | sleep()/wait(n) |
| TERMINATED | 终止 | 执行完毕 |
锁机制 #
synchronized vs ReentrantLock #
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM | API (AQS) |
| 锁类型 | 可重入锁 | 可重入锁 |
| 获取锁状态 | ❌ 无法判断 | ✅ tryLock() |
| 释放锁 | 自动 | 必须 finally 释放 |
| 公平锁 | ❌ 非公平 | ✅ 公平/非公平 |
| 响应中断 | ❌ | ✅ lockInterruptibly() |
| Condition | ❌ wait/notify | ✅ 多个Condition |
synchronized 锁升级 #
| 阶段 | 说明 | 场景 |
|---|---|---|
| 偏向锁 | Mark Word存储线程ID | 单线程重复执行 |
| 轻量级锁 | CAS自旋竞争 | 低竞争场景 |
| 重量级锁 | 内核mutex锁 | 高竞争场景 |
⚠️ 锁只能升级不能降级
ReentrantLock 示例 #
ReentrantLock lock = new ReentrantLock(); // 非公平锁
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
try {
lock.lockInterruptibly(); // 可中断
// 临界区
} finally {
lock.unlock();
}
// 条件变量
Condition condition = lock.newCondition();
try {
condition.await();
condition.signal();
} finally {
lock.unlock();
}
volatile 可见性 #
// 保证可见性 + 禁止指令重排序
private volatile boolean running = true;
// 底层实现
// 1. 内存屏障(Memory Barrier)
// 2. LOCK 前缀指令
特性:
- ✅ 保证可见性(主存读写)
- ✅ 禁止指令重排序
- ❌ 不保证原子性
适用场景:
- 状态标记位
- 单例模式(DCL)
- 简单读写
Atomic 原子类 #
| 类 | 用途 | 实现方式 |
|---|---|---|
AtomicInteger |
整数原子操作 | CAS + volatile |
AtomicLong |
长整数原子操作 | CAS + volatile |
AtomicReference |
引用原子操作 | CAS + volatile |
AtomicStampedReference |
带版本号的引用 | ABA问题解决 |
AtomicIntegerArray |
整数数组原子操作 | 数组+元素CAS |
线程池 #
面试高频 ⭐⭐⭐⭐⭐
七大参数详解 #
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, // 1. 核心线程数
maximumPoolSize, // 2. 最大线程数
keepAliveTime, // 3. 空闲线程存活时间
TimeUnit.SECONDS, // 4. 时间单位
workQueue, // 5. 工作队列
threadFactory, // 6. 线程工厂
handler // 7. 拒绝策略
);
工作流程图 #
参数详解 #
| 参数 | 默认值 | 说明 |
|---|---|---|
| corePoolSize | - | 核心线程数,常驻不销毁 |
| maximumPoolSize | - | 最大线程数 |
| keepAliveTime | 60s | 非核心线程空闲超时时间 |
| workQueue | - | 任务等待队列 |
| threadFactory | Default | 线程创建工厂 |
| handler | Abort | 饱和拒绝策略 |
常用队列 #
| 队列类型 | 特性 | 适用场景 |
|---|---|---|
ArrayBlockingQueue |
有界数组 | 防止资源耗尽 |
LinkedBlockingQueue |
无界链表 | 任务可无限堆积 |
SynchronousQueue |
不缓存 | 立即传递给线程 |
PriorityBlockingQueue |
优先级 | 任务有优先级 |
拒绝策略 #
| 策略 | 说明 |
|---|---|
AbortPolicy |
抛出异常(默认) |
CallerRunsPolicy |
调用者线程执行 |
DiscardPolicy |
直接丢弃 |
DiscardOldestPolicy |
丢弃最老任务 |
四大创建方式对比 #
// 1. FixedThreadPool(固定大小)
// core=maximum,无界队列,keepAlive=0
ExecutorService fixed = Executors.newFixedThreadPool(10);
// ⚠️ OOM风险:队列无限制
// 2. CachedThreadPool(缓存)
// core=0, maximum=∞,SynchronousQueue
ExecutorService cached = Executors.newCachedThreadPool();
// ⚠️ 线程爆炸风险:无限制创建线程
// 3. SingleThreadPool(单线程)
// core=maximum=1,无界队列
ExecutorService single = Executors.newSingleThreadExecutor();
// 4. ScheduledThreadPool(定时)
// 支持定时和周期任务
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(10);
最佳实践 #
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // CPU密集: CPU核心数 + 1
Runtime.getRuntime().availableProcessors() * 2, // IO密集: 2*CPU核心数
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 有界队列
new ThreadFactoryBuilder().setNameFormat("pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 优雅关闭
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
并发工具 #
CountDownLatch 门闩 #
// 6个线程,等待所有完成
CountDownLatch latch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
// 执行任务
latch.countDown(); // 计数减1
}).start();
}
latch.await(); // 等待计数为0
System.out.println("所有任务完成");
应用场景:
- 多任务并行后汇总结果
- 并发测试
CyclicBarrier 循环栅栏 #
// 7个线程,到齐后一起执行
CyclicBarrier barrier = new CyclicBarrier(7, () -> {
System.out.println("所有线程到齐,开始...");
});
for (int i = 0; i < 7; i++) {
new Thread(() -> {
// 执行任务
barrier.await(); // 等待其他线程
System.out.println("继续执行");
}).start();
}
应用场景:
- 多线程分批处理
- 阶段性任务
Semaphore 信号量 #
// 3个许可证
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可证
// 执行受限资源访问
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可证
}
}).start();
}
应用场景:
- 流量控制
- 数据库连接池
线程问题 #
线程安全保证 #
| 机制 | 原理 | 开销 |
|---|---|---|
| synchronized | monitor锁 | 高 |
| Lock | AQS | 中 |
| volatile | 内存屏障 | 低 |
| Atomic | CAS | 低 |
死锁条件与避免 #
// 死锁示例
Object lock1 = new Object();
Object lock2 = new Object();
// 线程1
synchronized (lock1) {
Thread.sleep(100);
synchronized (lock2) {} // 等待lock2
}
// 线程2
synchronized (lock2) {
Thread.sleep(100);
synchronized (lock1) {} // 等待lock1
}
死锁四个条件:
- 互斥条件
- 请求与保持
- 不可剥夺
- 循环等待
避免方案:
- 固定加锁顺序
- 锁超时
- 死锁检测
线程顺序执行 #
// 方案1: join()
Thread A = new Thread(() -> System.out.println("A"));
Thread B = new Thread(() -> System.out.println("B"));
Thread C = new Thread(() -> System.out.println("C"));
A.start();
A.join(); // 等待A完成
B.start();
B.join(); // 等待B完成
C.start();
// 方案2: CountDownLatch
CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);
new Thread(() -> {
System.out.println("A");
latch1.countDown();
}).start();
new Thread(() -> {
latch1.await();
System.out.println("B");
latch2.countDown();
}).start();
new Thread(() -> {
latch2.await();
System.out.println("C");
}).start();
// 方案3: 单线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("A"));
executor.submit(() -> System.out.println("B"));
executor.submit(() -> System.out.println("C"));
executor.shutdown();
虚拟线程 Virtual Thread(JDK 21+) #
Project Loom 项目成果,JDK 21 正式转正,轻量级用户态线程,海量并发
什么是虚拟线程? #
平台线程(传统线程):
- 由操作系统内核管理
1:1映射 - 栈内存默认 1MB,每个线程占用较大内存
- 调度由操作系统完成,上下文切换代价大
- 普通服务器几千个就到顶,无法百万并发
虚拟线程:
- 由 JVM 管理
M:N调度(M个虚拟线程映射到N个平台线程) - 栈初始仅几百字节,可动态扩容
- 调度在用户态完成,上下文切换极快
- 支持百万级并发,内存占用小
核心原理 #
1. 堆栈分割
虚拟线程的栈不连续:
- 栈帧分布在 Java 堆上
- 当虚拟线程阻塞时,JVM 将堆栈固定在堆
- 释放平台线程去运行其他虚拟线程
- 继续执行时恢复栈
2. 抢占式调度 vs 协作式调度
- 虚拟线程是抢占式调度,不由用户自己 yield
- JVM 自动调度,阻塞时自动让出平台线程
- 用户代码不需要修改就能享受好处
3. 载体线程 Carrier Thread
平台线程就是虚拟线程的"载体",一个载体可以跑多个虚拟线程。虚拟线程阻塞 → 载体跑去跑其他虚拟线程 → 阻塞解除 → 再找个载体继续跑。
代码示例 #
创建虚拟线程:
// 1. 直接创建
Thread vt = Thread.ofVirtual().start(() -> {
System.out.println("Hello virtual thread");
});
// 2. 虚拟线程线程池
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100000; i++) {
executor.submit(() -> {
// 处理请求,每个请求一个虚拟线程
return result;
});
}
executor.close();
// 3. Spring Boot 3.2+ 配置虚拟线程
spring:
threads:
virtual:
enabled: true # 开启后处理请求都是虚拟线程
每个请求一个虚拟线程:
- Web 服务器每个请求开一个虚拟线程
- 即使十万并发,内存也扛得住
- 编程模型还是同步阻塞,简单易懂
- 不需要改造成反应式(Netty+WebFlux)就能获得高并发
虚拟线程 vs 平台线程 对比 #
| 对比项 | 平台线程(传统) | 虚拟线程 |
|---|---|---|
| 映射 | 1:1 内核线程 | M:N 用户态 |
| 栈大小 | 1MB 固定 | 几百B ~ 数MB 动态 |
| 上下文切换 | 内核态,慢 | 用户态,快 |
| 最大并发 | 几千 | 几十万~几百万 |
| 内存占用 | 高 | 低 |
| 适用场景 | CPU密集,少量线程 | IO密集,海量并发 |
最佳实践 #
1. 适用场景 ✅ #
- Web 服务:每个请求一个线程,IO密集
- RPC 调用:大量等待IO的并发请求
- 批量处理:十万个任务并行处理
- 消息消费:多消费者并行处理
2. 不适用场景 ❌ #
- CPU 密集计算:没有阻塞,虚拟线程没优势
- 非常长生命周期线程:比如后台定时任务,平台线程足够
synchronized块内阻塞:JDK 21 之前虚拟线程在synchronized会 pinned 住平台线程,JDK 21+ 已经改进支持虚拟线程协作唤醒
3. 内存配置 #
虚拟线程大量创建,每个都有栈在堆:
- 如果百万虚拟线程,注意堆内存大小
-Xmx设置足够 - 栈自动扩容收缩,不用太担心
4. 依赖第三方库注意事项 #
- 如果第三方库使用
ThreadLocal,大量虚拟线程可能导致内存升高 - 短期请求处理完虚拟线程死亡,ThreadLocal 会清理,问题不大
- 长期池化虚拟线程需要注意内存泄漏
为什么虚拟线程好? #
传统平台线程 + 反应式(WebFlux)方案问题:
- 反应式编程模型复杂,回调/ Mono/Flux 难写难调试
- 对依赖库要求高,很多阻塞API不兼容
- 开发效率低,学习曲线陡
虚拟线程方案:
- 编程模型不变,还是同步阻塞写法
- JVM 底层搞定并发,用户代码不用改
- 同样获得百万并发高吞吐量
- 开发效率高,调试简单
常见问题 #
Q: 虚拟线程会完全取代平台线程吗? A: 不会。CPU密集计算还是用平台线程更好,没有额外调度开销。虚拟线程解决IO密集海量并发问题。
Q: 虚拟线程更快吗? A: 单个请求不一定更快,但高并发下吞吐量高很多,能支持更多并发请求。
Q: 虚拟线程有没有什么坑? A:
- 内存:百万虚拟线程需要更大堆
synchronized:旧JDK会pin住平台线程,JDK 21+修复- ThreadLocal:大量线程持有会增加内存使用
🎯 面试题汇总 #
基础 #
- 线程状态转换?
答:
Java线程在生命周期中共有6种状态(见java.lang.Thread.State枚举):
| 状态 | 说明 | 转换条件 |
|---|---|---|
| NEW | 新建状态 | 创建了Thread对象但还未调用start() |
| RUNNABLE | 可运行状态 | 调用start()后,等待CPU调度 |
| BLOCKED | 阻塞状态 | 等待获取synchronized锁 |
| WAITING | 无限等待 | wait()/join()/LockSupport.park() |
| TIMED_WAITING | 计时等待 | sleep(time)/wait(time)/parkNanos |
| TERMINATED | 终止状态 | 线程执行完毕 |
转换流程:
注意:
RUNNABLE包含了正在运行和可运行两种情况,操作系统层面区分,但Java层面合并为一个状态。
- sleep() 和 wait() 区别?
答:
| 区别点 | sleep() |
wait() |
|---|---|---|
| 所属类 | Thread类的静态方法 |
Object类的实例方法 |
| 释放锁 | ❌ 不释放锁 | ✅ 释放锁,进入等待池 |
| 唤醒条件 | 时间到自动唤醒 | 需要notify()/notifyAll()唤醒 |
| 使用场景 | 暂停指定时间 | 线程间通信,等待条件 |
| 异常处理 | 需要捕获InterruptedException |
需要捕获InterruptedException |
| 位置 | 可以在任何地方调用 | 只能在synchronized块中调用 |
核心区别: sleep()是线程暂停,不释放锁;wait()是线程等待,释放锁让其他线程抢锁。
- start() 和 run() 区别?
答:
| 区别点 | start() |
run() |
|---|---|---|
| 作用 | 启动新线程,调用本地方法start0()注册线程到操作系统 |
线程的执行体,普通方法调用 |
| 线程创建 | 真正创建新线程,进入就绪队列 | 还是在当前线程执行,没有新线程 |
| 调用次数 | 一个线程只能调用一次 | 可以被多次调用 |
| 运行方式 | 异步执行,并发运行 | 同步执行,在当前线程直接运行 |
示例:
// 正确:启动新线程
new Thread(() -> System.out.println("hello")).start();
// 错误:还是在主线程执行,没有并发
new Thread(() -> System.out.println("hello")).run();
锁机制 #
- synchronized 和 Lock 区别?
答:
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM底层实现,字节码monitorenter/monitorexit |
API层面实现,基于AQS队列同步器 |
| 锁类型 | 可重入 | 可重入 |
| 获取锁状态 | 无法判断是否获取到锁 | 可以通过tryLock()尝试获取锁,非阻塞 |
| 释放锁 | 自动释放(异常也会自动释放) | 必须手动在finally中释放,否则会死锁 |
| 公平锁 | 只有非公平锁,无法设置 | 支持公平/非公平两种模式,构造器可指定 |
| 响应中断 | 不支持,一直等待 | 支持lockInterruptibly()可响应中断 |
| 条件变量 | 只有一个条件队列,wait/notify |
支持多个Condition,可以精确唤醒 |
| 锁对象 | 可以修饰方法、代码块 | 只能是API对象 |
选择原则:
- synchronized适合简单并发场景,易用不易错
- ReentrantLock适合复杂场景:需要公平锁、超时获取、多个条件等
- synchronized 锁升级过程?
答:
为了减少锁的性能开销,JDK 1.6 对synchronized进行了优化,引入了锁升级过程:
各阶段说明:
| 阶段 | 说明 | 适用场景 | 实现 |
|---|---|---|---|
| 偏向锁 | Mark Word存储线程ID,只有一个线程进入时,不竞争就是偏向 | 单线程重复进入同步块 | CAS修改Mark Word |
| 轻量级锁 | 多个线程交替竞争,用CAS自旋尝试获取锁 | 低竞争场景,线程交替执行 | 在栈帧建锁记录,CAS替换Mark Word |
| 重量级锁 | CAS自旋多次失败后膨胀为重量级锁,依赖操作系统mutex | 高竞争场景,多线程同时抢锁 | 内核态阻塞唤醒 |
⚠️ 重要特性:锁只能升级不能降级,所以一旦膨胀为重量级锁就无法回去。
- volatile 作用和原理?
答:
两大作用:
- 保证可见性:一个线程修改了共享变量的值,其他线程能立即看到最新值
- 禁止指令重排序:禁止编译器和CPU对指令进行重排序优化
底层原理:
- 通过**内存屏障(Memory Barrier)**实现
- 写volatile后插入
store屏障,强制刷新到主存 - 读volatile前插入
load屏障,强制从主存读取 - CPU层面使用
LOCK前缀指令,触发缓存一致性协议(MESI),保证缓存行同步
特性限制:
- ❌ 不保证原子性:复合操作(如
count++)仍然需要锁 - ✅ 适用场景:状态标记位、双重检查锁定(DCL)单例模式
示例:
// 正确:状态标记,停止线程
private volatile boolean running = true;
public void stop() {
running = false; // 其他线程立即可见
}
public void run() {
while (running) { // 每次都从主存读,能正确停止
// do work
}
}
- CAS 和 ABA 问题?
答:
CAS是什么?
- CAS = Compare-And-Swap,比较并交换,是一种原子操作
- 底层:CPU指令支持(
cmpxchg指令),无锁并发的基础 - 原理:
V内存值,A预期值,B新值。如果V == A则V = B,返回成功;否则失败
ABA问题:
- 一个线程把值从
A改成B,另一个线程又改回A - 此时CAS检查发现值还是
A,误认为没有被修改过,实际上被改过 - 这在某些业务场景会导致问题
解决方法:
- 使用**版本号(邮票)**机制,每次修改都递增版本号
- Java中提供了
AtomicStampedReference类来解决
示例代码:
// 原始ABA问题
AtomicReference<String> ref = new AtomicReference<>("A");
ref.compareAndSet("A", "B"); // 成功
ref.compareAndSet("B", "A"); // 又改回A,CAS无法发现
// 解决:带版本号的原子引用
AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 1);
int[] stampHolder = new int[1];
String currentValue = ref.get(stampHolder);
int currentStamp = stampHolder[0];
// 比较不仅比较值,还要比较版本号
ref.compareAndSet(currentValue, "B",
currentStamp, currentStamp + 1);
线程池 #
- 线程池七大参数?
答:
ThreadPoolExecutor(
corePoolSize, // 1. 核心线程数
maximumPoolSize, // 2. 最大线程数
keepAliveTime, // 3. 空闲线程存活时间
TimeUnit unit, // 4. 时间单位
workQueue, // 5. 工作队列
threadFactory, // 6. 线程工厂
handler // 7. 拒绝策略
)
参数详解:
| 参数 | 说明 |
|---|---|
| corePoolSize | 核心线程数,核心线程创建后不会销毁,一直常驻 |
| maximumPoolSize | 线程池允许创建的最大线程数 |
| keepAliveTime | 非核心线程空闲超过这个时间就会被销毁回收 |
| unit | keepAliveTime的时间单位 |
| workQueue | 保存等待执行任务的阻塞队列 |
| threadFactory | 创建线程的工厂,可自定义线程名称方便排查 |
| handler | 队列满且线程数达到maximumPoolSize时的拒绝策略 |
- 工作流程?
答:
线程池的工作流程图:
详细步骤:
-
提交任务,如果核心线程没满 → 创建核心线程执行
-
如果核心线程已满 → 尝试把任务放入工作队列排队
-
如果工作队列也满了 → 看最大线程数有没有到顶
-
如果最大线程数没到顶 → 创建非核心线程执行
-
如果最大线程数也到顶 → 触发拒绝策略
-
如何合理配置线程池?
答:
根据任务的CPU密集型还是IO密集型来配置:
| 任务类型 | 说明 | 配置公式 | 示例(8核CPU) |
|---|---|---|---|
| CPU密集型 | 大量计算、逻辑处理,很少阻塞 | 核心线程数 = CPU核心数 + 1 |
8 + 1 = 9 |
| IO密集型 | 大量IO操作(网络、数据库),经常阻塞 | 核心线程数 = 2 × CPU核心数 或 核心线程数 = CPU核心数 / (1 - 阻塞系数) |
2 × 8 = 16 |
更多建议:
- 队列使用有界队列:防止任务无限堆积导致OOM
- 自定义线程工厂:给线程起有意义的名字,方便问题排查
- 拒绝策略使用
CallerRunsPolicy:让调用者线程自己执行,避免丢弃任务 - 优雅关闭:使用
shutdown()+awaitTermination(),最后shutdownNow()
最佳实践示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() * 2, // IO密集型
Runtime.getRuntime().availableProcessors() * 4,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 有界队列
new ThreadFactoryBuilder().setNameFormat("biz-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
- 为什么阿里规范禁止 Executors 创建线程池?
答:
Executors创建线程池存在资源耗尽风险:
| 创建方式 | 问题 | 风险 |
|---|---|---|
FixedThreadPool / SingleThreadPool |
使用LinkedBlockingQueue无界队列 |
任务可以无限堆积,最终导致OOM(OutOfMemoryError) |
CachedThreadPool / ScheduledThreadPool |
允许创建最大线程数为Integer.MAX_VALUE |
可以无限创建线程,最终导致OOM |
正确做法: 直接使用ThreadPoolExecutor构造器,手动指定参数:
- 使用有界队列,控制队列容量
- 限制最大线程数,防止线程爆炸
- 根据业务类型合理设置参数
这样更容易理解线程池运行原理,也避免了隐藏的资源风险。
进阶 #
- ThreadLocal 原理及内存泄漏?
答:
原理:
ThreadLocal并不存储值,每个Thread内部有一个ThreadLocalMapThreadLocal作为key,值作为value存在这个Map里- 不同线程访问互相隔离,每个线程只能看到自己的值
核心结构:
内存泄漏问题:
Entry继承WeakReference<ThreadLocal>,key是弱引用- 如果
ThreadLocal没有强引用,GC会回收key - 但value仍然存在强引用(来自Thread → ThreadLocalMap → Entry → value)
- key被回收后变成
null,但value无法访问,造成内存泄漏 - 尤其是在线程池中,线程会复用长期存活,泄漏会累积
解决方法:
- 用完
ThreadLocal后,必须调用remove()删除Entry - 最佳实践:
try-finally模式
try {
threadLocal.set(value);
// 业务逻辑
} finally {
threadLocal.remove(); // 必须清理
}
JDK 8 之后,
set()和rehash()会主动清理key为null的entry,但不能完全依赖。
- 如何捕获线程异常?
答:
有几种方式:
方式1:try-catch在run()方法内部捕获
new Thread(() -> {
try {
// 业务代码
} catch (Exception e) {
// 处理异常
log.error("线程异常", e);
}
}).start();
方式2:设置UncaughtExceptionHandler
Thread thread = new Thread(() -> {
throw new RuntimeException("异常");
});
thread.setUncaughtExceptionHandler((t, e) -> {
log.error("线程{}未捕获异常: {}", t.getName(), e);
});
thread.start();
方式3:线程池通过Future捕获
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<?> future = executor.submit(() -> {
throw new RuntimeException("任务异常");
});
try {
future.get(); // 这里会抛出ExecutionException包装异常
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // 获取原始异常
log.error("任务异常", cause);
}
方式4:自定义线程工厂,统一设置异常处理器
ThreadFactory factory = r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((thread, e) -> {
log.error("线程异常: {}", e.getMessage());
});
return t;
};
- 如何保证线程顺序执行?
答:
常见有三种方案:
方案1:使用join()方法
Thread A = new Thread(() -> System.out.println("A"));
Thread B = new Thread(() -> System.out.println("B"));
Thread C = new Thread(() -> System.out.println("C"));
A.start();
A.join(); // 等待A完成再继续
B.start();
B.join(); // 等待B完成再继续
C.start(); // A → B → C 顺序执行
方案2:使用CountDownLatch计数器
CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);
new Thread(() -> {
System.out.println("A");
latch1.countDown();
}).start();
new Thread(() -> {
latch1.await(); // 等待A完成
System.out.println("B");
latch2.countDown();
}).start();
new Thread(() -> {
latch2.await(); // 等待B完成
System.out.println("C");
}).start();
方案3:使用单线程线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("A"));
executor.submit(() -> System.out.println("B"));
executor.submit(() -> System.out.println("C"));
executor.shutdown();
单线程池保证任务按提交顺序排队执行。
方案4:使用CompletableFuture链式调用(Java 8+)
CompletableFuture.runAsync(() -> System.out.println("A"))
.thenRun(() -> System.out.println("B"))
.thenRun(() -> System.out.println("C"));
- 什么是虚拟线程?和平台线程区别?
答:
虚拟线程是JDK 21引入的轻量级线程,来自Project Loom项目,由JVM在用户态管理,不直接映射操作系统线程。
对比:
| 对比项 | 平台线程(传统线程) | 虚拟线程 |
|---|---|---|
| 映射关系 | 1:1 映射操作系统内核线程 | M:N 映射,M个虚拟线程映射N个平台线程 |
| 栈大小 | 默认1MB固定大小,大块分配 | 初始几百字节,堆上动态扩容 |
| 调度 | 操作系统内核调度 | JVM用户态调度,阻塞时自动卸载 |
| 上下文切换 | 内核态切换,代价高 | 用户态切换,代价极低 |
| 并发数量 | 几千个就到顶,受内存限制 | 支持几十万~几百万并发 |
| 内存占用 | 高,每个1MB | 低,动态分配 |
核心原理:
- 虚拟线程的栈放在Java堆上,不占用内核栈
- 当虚拟线程阻塞时,JVM将其栈快照固定在堆,释放载体平台线程
- 阻塞解除后,再找一个空闲平台线程继续执行
- 对用户代码透明,编程模型不变,还是同步阻塞写法
- 虚拟线程适用场景?最佳实践?
答:
✅ 适用场景:
- Web服务:每个请求一个虚拟线程,IO密集型场景,支持十万并发
- RPC/网络调用:大量等待IO的并发请求
- 批量处理:十万个任务并行处理
- 消息消费:多消费者并行处理队列消息
❌ 不适用场景:
- CPU密集计算:没有阻塞,虚拟线程调度没有优势,反而增加开销
- 非常长生命周期线程:后台定时任务等,平台线程足够
最佳实践:
-
每个任务一个虚拟线程
// Web服务:每个请求一个虚拟线程 ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); for (int i = 0; i < 100000; i++) { executor.submit(() -> handleRequest()); } -
Spring Boot 3.2+ 直接开启配置
spring: threads: virtual: enabled: true -
注意事项:
ThreadLocal:大量虚拟线程使用ThreadLocal会增加内存使用,短期任务问题不大synchronized:JDK 21之前,虚拟线程在synchronized块阻塞会pin住平台线程,JDK 21已修复- 内存:百万虚拟线程需要更大堆内存
-Xmx设置足够
优势总结:
- 保持同步阻塞编程模型,简单易懂,调试方便
- 不需要改造成复杂的反应式编程就能获得高吞吐量
- 开发效率高,内存效率好
🔗 相关笔记 #
最后更新: 2026-04-28