Java并发——线程间的通信

Java并发——线程间的通信 在多线程编程中线程间通信是一个核心话题。当多个线程需要协同完成某个任务时它们必须能够互相通知状态的变化以避免竞态条件和无效的资源占用。Java提供了多种线程间通信的方式从最基础的wait/notify机制到Lock配合Condition的灵活方案。本文将带你全面了解线程间通信的原理、常见陷阱以及如何优雅地实现线程协作。一、线程间通信的必要性思考一个简单的场景两个线程操作一个共享变量一个线程负责加1另一个线程负责减1要求交替执行10轮。如果没有通信机制线程A可能连续加多次线程B才减一次导致结果混乱。线程间通信正是为了解决这类问题——让线程在合适的时机暂停和唤醒从而保证操作的顺序性和数据的一致性。二、传统的wait/notify机制2.1 基本使用Java中每个对象都有一组监视器方法wait()、notify()、notifyAll()。它们必须在同步块synchronized中使用因为需要获取对象的监视器锁。下面是一个经典的“生产者-消费者”示例两个线程交替对变量进行1和-1操作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文档明确要求的。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改写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(); } } }相比synchronizedLock提供了更多控制能力如tryLock、可中断锁等而Condition则可以创建多个等待队列实现精确唤醒。3.2 多个Condition实现精准通信需求三个线程 A、B、C 依次执行A 打印5次B 打印10次C 打印15次循环10轮。这种场景下需要在线程A执行完后精确唤醒BB执行完后精确唤醒CC执行完后精确唤醒A。通过为每个线程创建一个Condition对象并结合一个状态标识即可轻松实现。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 实现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 实现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(); } } }五、总结与最佳实践优先使用LockCondition如果需要精确控制线程唤醒顺序、支持中断或超时或者需要更灵活的锁机制推荐使用ReentrantLock和Condition。避免虚假唤醒无论使用wait/notify还是Condition.await()判断条件时必须使用while循环而不是if。在finally中释放锁Lock.unlock()必须放在finally块中确保锁在任何情况下都能被释放避免死锁。使用多个Condition实现精确通信当需要多个线程协作时为每个线程创建独立的Condition结合状态标志可以显著提高代码的可读性和效率。注意notify()vsnotifyAll()使用Condition.signal()可以精确唤醒一个等待线程而signalAll()会唤醒所有等待该条件的线程。一般情况下精确唤醒能减少不必要的上下文切换。