线程同步与线程协作怎么理解:从锁、等待通知到异步任务编排
线程同步与线程协作怎么理解:从锁、等待通知到异步任务编排
面试指北 · 蔚来
题意
面试官问“线程同步和线程协作是怎么理解的”,通常不是想听两个背诵式定义,而是想确认三件事:
- 你能不能分清“共享资源安全”和“线程之间配合”这两个问题;
- 你能不能说出常见机制,以及这些机制分别在解决什么问题;
- 你能不能把概念落到工程场景里,比如等待通知、计数器、栅栏,以及
CompletableFuture、ListenableFuture这类异步任务编排工具。
可以先给出一条回答主线:线程同步解决“多个线程不要同时把共享数据改坏”,线程协作解决“多个线程如何按条件、按顺序一起完成一件事”。 二者不是对立的,协作往往要建立在同步的基础之上。
严格来说,Java 并发里的“同步”有广义和狭义之分。广义上,wait/notify 这种等待通知也属于同步机制;狭义上,面试中常把“保护共享资源”叫线程同步,把“线程间通信与任务配合”叫线程协作。回答时先说明这个边界,再按狭义展开,会更清楚。
结论
- 线程同步:核心是互斥访问共享资源,避免竞态条件,保证数据一致性和线程安全。典型手段是锁,如
synchronized、ReentrantLock,也包括Semaphore这类控制并发访问数量的工具。 - 线程协作:核心是线程间通信和执行时序配合,让线程在某个条件满足时等待,或在某个阶段完成后继续。典型手段是
wait/notify、CountDownLatch、CyclicBarrier等。 - 同步关注“安全”:同一时刻谁能进入临界区,共享变量是否可见、是否会被并发修改。
- 协作关注“配合”:谁先做、谁后做,谁等谁,多个任务什么时候汇合。
- 二者经常一起使用:
wait/notify必须配合锁使用;CountDownLatch、CyclicBarrier内部也依赖同步机制。 - 异步编排是任务级协作的延伸:
CompletableFuture和 Guava 的ListenableFuture解决的是“多个异步任务如何串联、组合、回调、异常处理”的问题,不等于替代锁。
要点展开
一、线程同步:先保证“别把共享数据改坏”
1. 定义与要解决的问题
线程同步用于解决多个线程访问共享资源时的安全性问题。多个线程同时读写同一个变量、同一个对象状态或同一个文件时,如果没有控制,就可能出现竞态条件:最终结果依赖线程调度的先后顺序,导致数据不一致。
同步机制要保证线程之间的互斥性和一定的有序性,避免资源冲突。比如两个线程同时对 count++,这个操作看起来是一行代码,实际上包含“读取、计算、写回”多个步骤,并发执行时可能互相覆盖。
2. 三个关键词:互斥、可见性、有序性
- 互斥性:保证某一时刻只有一个线程能进入临界区访问共享资源,其他线程需要等待。
- 可见性:一个线程对共享变量的修改,其他线程能够及时看到。
synchronized、volatile以及Lock都能提供一定的可见性保证。 - 有序性:通过锁和 happens-before 规则,避免临界区内的操作被随意重排到锁外,从而保证并发语义正确。
需要注意:volatile 可以保证可见性并禁止部分指令重排,但它不保证复合操作的原子性,所以不能简单用 volatile 替代锁。
3. 性能影响
同步会带来性能开销。竞争不激烈时,JVM 会做锁优化;竞争激烈时,线程可能阻塞、唤醒、上下文切换,吞吐量下降。所以同步的原则是:缩小临界区范围,但不要为了性能牺牲正确性。
4. 常见机制
(1)synchronized
Java 内置关键字,可用于方法或代码块级别同步,依赖对象监视器锁。它使用简单,能保证原子性、可见性和有序性,并且是可重入的。
public synchronized void synchronizedMethod() {
// 同一时刻只有一个线程能进入该实例方法
}
也可以写成同步代码块,锁住更小的范围:
synchronized (lock) {
// 临界区:访问共享资源
}
(2)Lock 接口
如 ReentrantLock,比 synchronized 更灵活,支持可中断锁、超时锁、公平锁/非公平锁,以及配合 Condition 做条件等待。适合需要精细控制锁行为的场景。
(3)信号量 Semaphore
通过控制许可数量限制对资源的访问。它更像“并发访问配额”,而不是传统意义上只允许一个线程进入的互斥锁。例如资源池有 3 个连接,就可以用 3 个许可限制同时访问的线程数。使用时要注意 acquire() 和 release() 成对出现。
二、线程协作:再解决“线程之间怎么配合”
1. 定义
线程协作用于解决多个线程之间的协调工作问题。它让线程按照预定条件或顺序配合完成任务,例如:
- 生产者生产数据后通知消费者;
- 主线程等待多个子线程全部完成;
- 一组线程到达某个阶段后一起继续;
- 多个异步任务完成后汇总结果。
线程协作的特点包括:线程间通信、并发执行、协调性。它关注的不是“谁能改共享变量”,而是“什么时候该等待、什么时候该继续、任务之间如何衔接”。
2. 常见机制
(1)wait() 和 notify()
wait/notify 是对象监视器上的等待通知机制。线程在条件不满足时进入等待状态,释放锁;其他线程修改条件后通知它。关键点:
wait()、notify()、notifyAll()必须在持有同一对象锁的同步块或同步方法中调用,否则会抛IllegalMonitorStateException;wait()会释放锁并进入等待;notify()不会立即释放锁,要等当前同步块执行完;- 条件判断要用
while而不是if,防止虚假唤醒; - 一般更推荐
notifyAll(),除非非常明确只需要唤醒一个线程。
synchronized (lock) {
while (!condition) {
lock.wait(); // 等待条件满足,同时释放 lock
}
// 条件满足,处理任务
lock.notify(); // 唤醒其他等待同一 lock 的线程
}
(2)CountDownLatch
用于一个或多个线程等待其他线程完成。它维护一个倒计数,countDown() 将计数减一,await() 阻塞直到计数归零。典型场景是主线程等待多个子线程执行完毕。
注意:CountDownLatch 是一次性的,计数归零后不能重置。下面用 3 个子线程示意:
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
// 子线程任务
} finally {
latch.countDown(); // 每个子线程完成后计数减一
}
}).start();
}
latch.await(); // 主线程等待计数器归零
System.out.println("三个子线程都完成了");
(3)CyclicBarrier
允许一组线程到达某个同步点后再继续执行,适合阶段性任务协同。它和 CountDownLatch 的区别是:CyclicBarrier 是“一组线程互相等待”,并且可以循环复用;CountDownLatch 更像“一个或多个线程等计数归零”,通常一次性使用。
CyclicBarrier 还可以设置 barrier action,在所有线程到齐后、继续执行前先执行一个动作,适合做阶段汇总。
三、同步与协作的区别
| 方面 | 线程同步 | 线程协作 |
|---|---|---|
| 目标 | 保证资源访问的安全性和一致性 | 协调线程间的工作配合 |
| 核心问题 | 共享资源竞争、竞态条件 | 线程通信、等待唤醒、执行顺序 |
| 机制 | 依赖锁,如 synchronized、Lock,以及 Semaphore |
wait/notify、CountDownLatch、CyclicBarrier 等 |
| 关注点 | 数据一致性、资源互斥、可见性 | 线程之间的执行顺序和任务协调 |
| 典型场景 | 多线程修改账户余额、库存、缓存状态 | 生产者消费者、主线程等子任务、分阶段并行计算 |
| 一句话理解 | “别同时改坏共享数据” | “按条件、按阶段一起完成任务” |
需要强调:二者不是割裂的。比如 wait/notify 本身就必须在同步块中使用;CountDownLatch 的计数操作也需要保证线程安全。面试时先讲区别,再补一句“协作通常建立在同步机制之上”,会更完整。
四、延伸:任务级协作——CompletableFuture 与 ListenableFuture
当讨论从“线程级协作”上升到“异步任务编排”时,常会提到 CompletableFuture 和 Guava 的 ListenableFuture。它们不是锁的替代品,而是解决多个异步任务如何组合、回调、异常处理和汇聚的问题。
1. CompletableFuture(Java 标准库)
CompletableFuture 是 Java 8 引入的异步编程工具,支持非阻塞式任务组合和异常处理。
核心功能:
- 异步任务执行:使用
supplyAsync()或runAsync()提交异步任务。
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("执行异步任务");
});
- 任务组合:通过
thenApply()、thenCombine()等方法,将多个异步任务串联或组合。
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "任务1结果");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "任务2结果");
CompletableFuture<String> combined = future1.thenCombine(future2, (result1, result2) -> {
return result1 + " + " + result2;
});
System.out.println(combined.join());
- 异常处理:支持链式异常处理,提供
exceptionally()、handle()等方法。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
boolean fail = true;
if (fail) {
throw new RuntimeException("异常发生");
}
return "正常结果";
}).exceptionally(ex -> {
return "异常结果:" + ex.getMessage();
});
System.out.println(future.join());
- 多任务汇聚:使用
allOf()或anyOf()方法并行执行多个任务并汇总结果。
CompletableFuture<Void> allOf = CompletableFuture.allOf(
CompletableFuture.runAsync(() -> System.out.println("任务1")),
CompletableFuture.runAsync(() -> System.out.println("任务2"))
);
allOf.join();
适用场景:复杂任务依赖流、异步任务组合与结果聚合、异常处理与回退机制。使用时要注意:allOf() 返回的是 CompletableFuture<Void>,它只表示“都完成了”,如果要收集各任务结果,还需要再从各个 future 中取;join() 不抛受检异常,get() 会抛 InterruptedException、ExecutionException;默认异步任务可能使用 ForkJoinPool.commonPool(),耗时或阻塞任务应显式传入线程池。
2. ListenableFuture(Guava 工具包)
ListenableFuture 是 Google Guava 提供的增强版 Future,主要用于在异步任务完成后添加回调函数进行非阻塞处理。
核心功能:
- 回调机制:使用
addListener()方法添加回调函数,任务完成后自动执行回调。
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(3));
ListenableFuture<String> future = service.submit(() -> "任务完成");
future.addListener(() -> {
System.out.println("回调执行");
}, service);
- 增强的回调处理:借助
Futures.addCallback()方法,支持更灵活的回调处理,包括成功和失败的分支。
Futures.addCallback(future, new FutureCallback<String>() {
@Override
public void onSuccess(String result) {
System.out.println("成功:" + result);
}
@Override
public void onFailure(Throwable t) {
System.out.println("失败:" + t.getMessage());
}
}, service);
- 任务组合:使用
Futures.transform()或Futures.allAsList()进行任务结果转换或任务组合。
ListenableFuture<String> future1 = service.submit(() -> "结果1");
ListenableFuture<String> future2 = service.submit(() -> "结果2");
ListenableFuture<List<String>> combined = Futures.allAsList(future1, future2);
combined.addListener(() -> {
System.out.println("所有任务完成");
}, service);
适用场景:需要非阻塞回调机制、轻量级异步任务组合与结果处理,或者项目已经在使用 Guava、又需要兼容较早 Java 版本的场景。
3. CompletableFuture 与 ListenableFuture 对比
| 特性 | CompletableFuture |
ListenableFuture |
|---|---|---|
| 支持的 Java 版本 | Java 8+ | Guava 依赖,可支持更早版本的 Java |
| 回调机制 | 使用 thenApply、thenAccept 等链式流式回调 |
通过 addListener 和 Futures.addCallback |
| 任务组合 | 支持复杂任务组合,如 thenCombine、thenCompose、allOf、anyOf |
支持轻量级任务组合,如 Futures.transform、Futures.allAsList |
| 异常处理 | 内置异常处理,如 exceptionally、handle、whenComplete |
需要通过 FutureCallback 的 onFailure 或组合方法处理 |
| 灵活性与扩展性 | 更强,适合复杂任务编排 | 简单易用,适合轻量场景 |
| 依赖 | JDK 标准库 | 需要引入 Guava |
| 选择建议 | 新项目、Java 8+ 优先考虑 | 已有 Guava 体系或需要兼容旧 Java 时考虑 |
易错点与对比
-
把“同步”只理解成“加锁排队”
同步不仅解决互斥,还涉及可见性和有序性。volatile能保证可见性、禁止部分重排,但不保证count++这种复合操作的原子性。 -
wait/notify使用错误
wait/notify必须在持有同一对象锁时调用;wait()会释放锁,notify()不会立即释放锁;条件判断要用while防虚假唤醒;不确定唤醒哪个线程时优先notifyAll()。 -
混淆
CountDownLatch和CyclicBarrier
CountDownLatch是倒计数,一个或多个线程等计数归零,通常一次性;CyclicBarrier是一组线程互相等待到齐,可循环复用,还能设置 barrier action。 -
把
Semaphore当普通互斥锁
Semaphore控制的是许可数量,允许多个线程同时访问有限资源;它不是“谁加锁谁解锁”的所有权模型,但acquire/release仍要严格配对。 -
CompletableFuture的线程池与异常处理
runAsync、supplyAsync不传线程池时可能使用公共池;阻塞型任务应显式指定业务线程池。join()不抛受检异常,get()会抛受检异常;allOf()只表示全部完成,结果还要逐个取;anyOf()返回最先完成的任务结果。 -
ListenableFuture的回调执行线程
addListener和Futures.addCallback都需要传入Executor,回调在哪个线程执行由这个Executor决定。不要把耗时逻辑直接堆在回调线程里,必要时切换到业务线程池。 -
同步与协作的对比不要绝对化
同步关注共享资源安全,协作关注线程间配合;但协作机制经常依赖同步机制。回答时说清“狭义区别”和“实际结合使用”,比简单二分会更稳。
小结
面试中可以把回答压缩成一套模板:
- 先定义:线程同步解决共享资源竞争下的安全问题;线程协作解决线程之间的通信、等待和顺序配合问题。
- 再讲机制:同步常用
synchronized、Lock、Semaphore;协作常用wait/notify、CountDownLatch、CyclicBarrier。 - 再讲区别:同步关注数据一致性和互斥,协作关注执行顺序和任务协同;二者经常结合使用。
- 最后延伸:
CompletableFuture和ListenableFuture是异步任务编排工具,属于“任务级协作”的工程延伸,使用时重点关注线程池、回调线程、异常处理和结果汇聚。
一句话收尾:线程同步是“让共享资源访问得安全”,线程协作是“让多个线程配合得顺畅”;锁是基础,等待通知和同步工具是编排,异步编排工具则把这种配合提升到了任务层面。