Java 线程间通信,简单说就是多个线程如何协调工作、交换数据、控制执行顺序。因为线程是并发执行的,如果不加控制,很容易出现数据错乱、重复执行、死锁等问题。
下面按"常用方式 → 底层原理 → 典型场景"来讲。
一、为什么需要线程间通信
多个线程共享进程内存,但每个线程有自己的工作内存(CPU 缓存、寄存器等)。如果一个线程改了共享变量,另一个线程可能看不到,或者看到的是旧值。
典型问题:
- 生产者先生产,消费者才能消费
- 一个线程必须等另一个线程完成才能继续
- 多个线程要互斥访问同一个资源
- 线程之间要传递结果
这些都需要线程间通信机制。
二、主要通信方式
1. synchronized + wait/notify/notifyAll
这是最经典的 Java 线程通信方式,基于对象监视器(Monitor)。
三个方法都必须在 synchronized 块或方法中调用,否则抛 IllegalMonitorStateException。
wait():释放锁并进入等待队列,直到被唤醒notify():随机唤醒一个等待该对象的线程notifyAll():唤醒所有等待该对象的线程
经典生产者-消费者模型:
java
class Buffer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 5;
public synchronized void produce(int value) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // 满了,生产者等待
}
queue.offer(value);
System.out.println("生产: " + value);
notifyAll(); // 唤醒消费者
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 空了,消费者等待
}
int value = queue.poll();
System.out.println("消费: " + value);
notifyAll(); // 唤醒生产者
return value;
}
}
关键点:
- 用
while而不是if判断条件,防止虚假唤醒 - 生产者和消费者等待的是同一个锁对象
notifyAll()比notify()更安全,避免唤醒错线程导致死等
2. ReentrantLock + Condition
Condition 可以理解为更灵活的 wait/notify,一个锁可以创建多个等待队列。
java
class Buffer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 5;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void produce(int value) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await();
}
queue.offer(value);
notEmpty.signalAll();
} finally {
lock.unlock();
}
}
public int consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
int value = queue.poll();
notFull.signalAll();
return value;
} finally {
lock.unlock();
}
}
}
优点:
- 可以精确唤醒某一类线程,比如只唤醒生产者或只唤醒消费者
- 支持公平锁、可中断等待、超时等待
- 比
synchronized更灵活
3. volatile + CAS / 原子类
volatile 保证可见性和有序性,但不保证原子性。
适合场景:
- 一个线程写,多个线程读的状态标志
- 配合
AtomicInteger、AtomicReference等原子类做无锁并发
java
class Flag {
private volatile boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) {
// do something
}
}
}
注意:volatile 不能替代锁。像 i++ 这种复合操作,即使变量是 volatile,也不是线程安全的。
4. BlockingQueue 阻塞队列
这是实际开发中最推荐的生产者-消费者实现方式,底层已经封装好了等待/唤醒逻辑。
常用实现:
ArrayBlockingQueue:有界数组队列LinkedBlockingQueue:链表队列,可选有界SynchronousQueue:不存储元素,直接传递PriorityBlockingQueue:优先级队列DelayQueue:延迟队列
java
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
// 生产者
new Thread(() -> {
try {
queue.put(1); // 满了会阻塞
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// 消费者
new Thread(() -> {
try {
Integer value = queue.take(); // 空了会阻塞
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
优点:
- 不用手写
wait/notify,减少出错 - 线程安全,性能较好
- 支持超时、中断
5. CountDownLatch / CyclicBarrier / Semaphore
这些是并发工具类,用于更复杂的线程协调。
CountDownLatch:一个线程等多个线程完成。
java
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// do something
latch.countDown();
}).start();
}
latch.await(); // 主线程等待三个线程完成
CyclicBarrier:多个线程互相等待,到齐后一起继续,可重复使用。
java
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到齐");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// do something
barrier.await();
}).start();
}
Semaphore:控制同时访问某个资源的线程数量。
java
Semaphore semaphore = new Semaphore(2); // 最多两个线程同时访问
semaphore.acquire();
try {
// 访问资源
} finally {
semaphore.release();
}
6. CompletableFuture 和 Future
用于线程间传递异步计算结果。
java
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
return 42;
});
future.thenAccept(result -> System.out.println(result));
适合任务编排、异步回调、多任务合并等场景。
7. Thread.join()
让当前线程等待另一个线程结束。
java
Thread t = new Thread(() -> {
// do something
});
t.start();
t.join(); // 主线程等 t 执行完
底层原理是 wait/notify,当线程结束时 JVM 会调用 notifyAll()。
8. LockSupport.park/unpark
更底层的线程阻塞/唤醒工具,不需要锁,也不要求顺序。
java
Thread t = new Thread(() -> {
LockSupport.park(); // 阻塞
System.out.println("被唤醒");
});
t.start();
LockSupport.unpark(t); // 唤醒指定线程
特点:
unpark可以先于park调用,许可证机制- 是
LockSupport的基础,AQS 内部大量使用
三、底层原理简析
1. 对象监视器(Monitor)
每个 Java 对象都有一个监视器,synchronized 就是基于它实现的。
- 竞争锁失败的线程进入 EntryList
- 调用
wait()的线程进入 WaitSet notify/notifyAll把 WaitSet 中的线程移到 EntryList 重新竞争锁
2. AQS(AbstractQueuedSynchronizer)
ReentrantLock、Semaphore、CountDownLatch 等都基于 AQS。
核心思想:
- 一个
volatile int state表示同步状态 - 一个 CLH 双向队列存放等待线程
- 通过 CAS 修改 state
- 获取失败则入队并
LockSupport.park
3. 内存可见性
volatile:写操作立即刷新到主内存,读操作从主内存读取synchronized:释放锁前把共享变量刷回主内存,获取锁时从主内存读取final:正确构造后对其他线程可见
四、如何选择
| 场景 | 推荐方式 |
|---|---|
| 简单等待/唤醒 | synchronized + wait/notify |
| 需要多个等待条件 | ReentrantLock + Condition |
| 生产者-消费者 | BlockingQueue |
| 一个线程等多个线程 | CountDownLatch |
| 多个线程互相等待 | CyclicBarrier |
| 控制并发数量 | Semaphore |
| 异步结果传递 | CompletableFuture |
| 简单状态标志 | volatile |
| 底层阻塞唤醒 | LockSupport |
五、常见坑
wait/notify不在synchronized中调用 → 抛异常- 用
if判断等待条件 → 虚假唤醒导致错误 notify唤醒错线程 → 可能死等,优先用notifyAllvolatile当锁用 → 复合操作不安全- 忘记
unlock→ 用try/finally - 线程中断被吞掉 → 捕获
InterruptedException后要么处理,要么重新设置中断标志 - 死锁 → 多把锁顺序不一致、锁嵌套等
六、一句话总结
Java 线程间通信的核心是:共享内存 + 同步机制 。简单场景用 synchronized + wait/notify,复杂场景用 Lock + Condition,生产者-消费者优先用 BlockingQueue,线程协调用 CountDownLatch/CyclicBarrier/Semaphore,异步编排用 CompletableFuture。底层都离不开可见性、原子性和有序性这三个问题。