在多线程编程中,线程间通信是一个核心话题。当多个线程需要协同完成某个任务时,它们必须能够互相通知状态的变化,以避免竞态条件和无效的资源占用。Java提供了多种线程间通信的方式,从最基础的 wait/notify 机制,到 Lock 配合 Condition 的灵活方案。本文将带你全面了解线程间通信的原理、常见陷阱以及如何优雅地实现线程协作。
一、线程间通信的必要性
思考一个简单的场景:两个线程操作一个共享变量,一个线程负责加1,另一个线程负责减1,要求交替执行10轮。如果没有通信机制,线程A可能连续加多次,线程B才减一次,导致结果混乱。线程间通信正是为了解决这类问题------让线程在合适的时机暂停和唤醒,从而保证操作的顺序性和数据的一致性。
二、传统的wait/notify机制
2.1 基本使用
Java中每个对象都有一组监视器方法:wait()、notify()、notifyAll()。它们必须在同步块(synchronized)中使用,因为需要获取对象的监视器锁。
下面是一个经典的"生产者-消费者"示例,两个线程交替对变量进行+1和-1操作:
java
class ShareData {
private int number = 0;
public synchronized void increment() throws InterruptedException {
// 1. 判断
if (number != 0) {
this.wait();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + " => " + number);
// 3. 通知
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
if (number != 1) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + " => " + number);
this.notifyAll();
}
}
public class WaitNotifyDemo {
public static void main(String[] args) {
ShareData data = new ShareData();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); }
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); }
}
}, "B").start();
}
}
运行结果会交替输出 A => 1 和 B => 0,共10轮。这里的关键点在于:
-
线程在执行操作前,先判断条件是否满足(number是否为0或1)。
-
不满足则调用
wait()进入等待状态,同时释放锁。 -
操作完成后,调用
notifyAll()唤醒所有等待的线程。
2.2 虚假唤醒问题
当我们将线程数增加到4个(两个加线程,两个减线程),并运行多次后,可能会看到 2、3 等异常值,甚至出现负数。这是因为 if 判断导致的虚假唤醒。
虚假唤醒 指的是线程被唤醒后,条件可能已经不再满足,但程序仍然继续执行。例如,当 number 为0时,A1和A2都等待在 increment 方法中;当B执行减1后调用 notifyAll(),A1和A2同时被唤醒,它们都从 wait() 后继续执行,导致 number 被连续加了两次,变为2。
解决方案 :将 if 改为 while,使线程被唤醒后重新检查条件。这是JDK文档明确要求的。
java
public synchronized void increment() throws InterruptedException {
while (number != 0) { // 使用while
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + " => " + number);
this.notifyAll();
}
2.3 wait/notify的局限性
-
无法精确唤醒 :
notifyAll()会唤醒所有等待线程,增加了不必要的上下文切换;notify()只唤醒一个,但无法指定唤醒哪一个。 -
必须与synchronized绑定 :只能配合
synchronized使用,不够灵活。 -
无法响应中断 :
wait()会抛出InterruptedException,但线程无法在等待期间主动中断。
三、Lock + Condition:更灵活的通信方式
从JDK 1.5开始,java.util.concurrent.locks 包提供了 Lock 接口和 Condition 接口,弥补了 wait/notify 的不足。
3.1 Condition的基本用法
每个 Condition 对象都相当于一个"队列",通过 await() 和 signal()/signalAll() 实现线程的等待与唤醒。与 wait/notify 类似,使用前必须先获取对应的锁。
将上面的例子用 ReentrantLock 和 Condition 改写:
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ShareData {
private int number = 0;
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + " => " + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number != 1) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + " => " + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
}
相比 synchronized,Lock 提供了更多控制能力(如 tryLock、可中断锁等),而 Condition 则可以创建多个等待队列,实现精确唤醒。
3.2 多个Condition实现精准通信
需求:三个线程 A、B、C 依次执行,A 打印5次,B 打印10次,C 打印15次,循环10轮。
这种场景下,需要在线程A执行完后精确唤醒B,B执行完后精确唤醒C,C执行完后精确唤醒A。通过为每个线程创建一个 Condition 对象,并结合一个状态标识,即可轻松实现。
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ShareResource {
private int flag = 1; // 1: A, 2: B, 3: C
private final Lock lock = new ReentrantLock();
private final Condition conditionA = lock.newCondition();
private final Condition conditionB = lock.newCondition();
private final Condition conditionC = lock.newCondition();
public void print5() {
lock.lock();
try {
while (flag != 1) {
conditionA.await();
}
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " => " + i);
}
flag = 2;
conditionB.signal(); // 唤醒B
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
while (flag != 2) {
conditionB.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " => " + i);
}
flag = 3;
conditionC.signal(); // 唤醒C
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
while (flag != 3) {
conditionC.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + " => " + i);
}
flag = 1;
conditionA.signal(); // 唤醒A
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ConditionDemo {
public static void main(String[] args) {
ShareResource resource = new ShareResource();
new Thread(() -> {
for (int i = 0; i < 10; i++) resource.print5();
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) resource.print10();
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) resource.print15();
}, "C").start();
}
}
这样,每个线程只会在属于自己的标识位被设置时才执行,执行完后精确唤醒下一个线程,避免了无效的唤醒竞争。
四、经典面试题:交替打印数字和字母
题目:两个线程,一个打印1~52的数字,另一个打印A~Z的字母,要求打印结果为12A34B...5152Z。
分析 :数字线程每次打印两个数字,字母线程每次打印一个字母。可以通过一个标志位来控制切换,也可以用 Condition 来实现精确交替。
4.1 使用 wait/notify 实现
java
class Printer {
private int num = 1;
private char letter = 'A';
private boolean printNum = true;
public synchronized void printNumber() {
for (int i = 0; i < 26; i++) {
while (!printNum) {
try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
}
System.out.print(num++);
System.out.print(num++);
printNum = false;
notifyAll();
}
}
public synchronized void printLetter() {
for (int i = 0; i < 26; i++) {
while (printNum) {
try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
}
System.out.print(letter++);
printNum = true;
notifyAll();
}
}
}
public class PrintDemo {
public static void main(String[] args) {
Printer printer = new Printer();
new Thread(printer::printNumber).start();
new Thread(printer::printLetter).start();
}
}
4.2 使用 Condition 实现
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Printer {
private int num = 1;
private char letter = 'A';
private boolean printNum = true;
private final Lock lock = new ReentrantLock();
private final Condition numberCondition = lock.newCondition();
private final Condition letterCondition = lock.newCondition();
public void printNumber() {
lock.lock();
try {
for (int i = 0; i < 26; i++) {
while (!printNum) {
numberCondition.await();
}
System.out.print(num++);
System.out.print(num++);
printNum = false;
letterCondition.signal();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printLetter() {
lock.lock();
try {
for (int i = 0; i < 26; i++) {
while (printNum) {
letterCondition.await();
}
System.out.print(letter++);
printNum = true;
numberCondition.signal();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
五、总结与最佳实践
-
优先使用
Lock+Condition如果需要精确控制线程唤醒顺序、支持中断或超时,或者需要更灵活的锁机制,推荐使用
ReentrantLock和Condition。 -
避免虚假唤醒
无论使用
wait/notify还是Condition.await(),判断条件时必须使用while循环,而不是if。 -
在
finally中释放锁
Lock.unlock()必须放在finally块中,确保锁在任何情况下都能被释放,避免死锁。 -
使用多个
Condition实现精确通信当需要多个线程协作时,为每个线程创建独立的
Condition,结合状态标志,可以显著提高代码的可读性和效率。 -
注意
notify()vsnotifyAll()使用
Condition.signal()可以精确唤醒一个等待线程,而signalAll()会唤醒所有等待该条件的线程。一般情况下,精确唤醒能减少不必要的上下文切换。