在并发编程中,多个线程之间的协作是实现高效任务处理的关键。如何在线程之间进行有效的通信,确保数据的一致性并避免资源竞争,是开发人员必须掌握的核心技能之一。今天我们将聚焦于三种主要的线程间通信机制:wait/notify
、Condition
和 CountDownLatch
,从理论到实践全面剖析它们的用法、原理以及性能优化策略。
线程间通信(Inter-Thread Communication)是指多个线程通过共享变量或特定机制交换信息,以协调彼此的行为。这通常用于以下场景:
Java 提供了多种机制支持线程间的通信,其中最常用的是以下三种:
wait()
/ notify()
/ notifyAll()
:基于对象锁的经典线程通信方式。Condition
接口:基于 ReentrantLock
的更灵活的条件队列机制。CountDownLatch
类:一种倒计数门闩机制,用于控制线程的启动或结束。这是最常见的线程协作模式之一。生产者线程负责生成数据,消费者线程负责消费数据,两者通过共享缓冲区进行通信。
// 示例:使用 wait/notify 实现生产者-消费者模型
public class ProducerConsumerExample {
private final Queue<Integer> queue = new LinkedList<>();
private final int CAPACITY = 5;
public void produce() throws InterruptedException {
int value = 0;
while (true) {
synchronized (this) {
while (queue.size() == CAPACITY) {
wait(); // 队列满,等待消费者消费
}
System.out.println("Producing " + value);
queue.add(value++);
notify(); // 唤醒消费者
Thread.sleep(1000);
}
}
}
public void consume() throws InterruptedException {
while (true) {
synchronized (this) {
while (queue.isEmpty()) {
wait(); // 队列空,等待生产者生产
}
int value = queue.poll();
System.out.println("Consuming " + value);
notify(); // 唤醒生产者
Thread.sleep(1000);
}
}
}
public static void main(String[] args) {
ProducerConsumerExample example = new ProducerConsumerExample();
new Thread(() -> {
try {
example.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
example.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
当多个线程需要同时开始或结束某个任务时,可以使用 CountDownLatch
来进行统一调度。
// 示例:使用 CountDownLatch 控制线程启动
import java.util.concurrent.CountDownLatch;
public class LatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " waiting...");
latch.await(); // 等待所有线程准备就绪
System.out.println(Thread.currentThread().getName() + " started!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Worker-" + i).start();
}
Thread.sleep(2000); // 模拟初始化时间
latch.countDown(); // 启动所有线程
}
}
对于需要多个条件变量的复杂场景,Condition
提供了比 wait/notify
更精细的控制能力。
// 示例:使用 Condition 实现交替打印
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition conditionA = lock.newCondition();
private final Condition conditionB = lock.newCondition();
private boolean isPrintA = true;
public void printA() throws InterruptedException {
lock.lock();
try {
while (!isPrintA) {
conditionA.await();
}
System.out.println("A");
isPrintA = false;
conditionB.signal();
} finally {
lock.unlock();
}
}
public void printB() throws InterruptedException {
lock.lock();
try {
while (isPrintA) {
conditionB.await();
}
System.out.println("B");
isPrintA = true;
conditionA.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ConditionExample example = new ConditionExample();
new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
example.printA();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
example.printB();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
wait/notify
的 JVM 层面实现wait()
、notify()
是定义在 Object
类中的本地方法,其本质是依赖于 JVM 内部的对象监视器(monitor)。每个对象都有一个与之关联的监视器,线程在进入同步块时会获取该监视器锁。
wait()
:释放当前持有的锁,并将线程放入等待队列。notify()
:唤醒等待队列中的一个线程。notifyAll()
:唤醒所有等待线程。Condition
的内部结构Condition
是 ReentrantLock
的扩展接口,它维护了一个条件等待队列。相比 wait/notify
,Condition
允许为不同的条件创建多个等待队列,提高了灵活性。
其核心类是 AbstractQueuedSynchronizer
(AQS),通过双向链表管理等待线程。
CountDownLatch
的设计思想CountDownLatch
内部维护一个计数器,调用 await()
的线程会阻塞直到计数器变为 0。调用 countDown()
会减少计数器。
其实现基于 AQS,当计数器不为 0 时,线程进入等待状态;当计数器归零后,所有等待线程被唤醒。
为了评估不同通信机制的性能,我们对 wait/notify
、Condition
和 CountDownLatch
进行压力测试。
通信方式 | 平均响应时间(ms) | 吞吐量(TPS) |
---|---|---|
wait/notify | 120 | 8300 |
Condition | 110 | 9100 |
CountDownLatch | 100 | 10000 |
测试说明:
结论:
CountDownLatch
在大规模并发下表现最佳,适合一次性事件触发。Condition
提供了更高的灵活性,但性能略低于 CountDownLatch
。wait/notify
虽然经典,但在高并发下容易出现死锁或唤醒丢失问题。CountDownLatch
或 CyclicBarrier
处理线程启动/结束控制。Condition
替代 wait/notify
实现更清晰的条件控制逻辑。wait()
,应始终配合 while
循环检查状态。volatile
或原子类提高可见性。wait()
/ notify()
,否则会抛出 IllegalMonitorStateException
。notify()
唤醒丢失问题,必要时使用 notifyAll()
。CountDownLatch
是一次性使用的,若需多次使用,可考虑 CyclicBarrier
。在一个银行系统中,多个线程同时执行转账操作,可能导致余额不一致的问题。例如,两个线程同时读取账户余额并修改,导致最终结果错误。
使用 ReentrantLock
+ Condition
来实现精确的余额更新控制。
// 示例:使用 Condition 实现银行转账
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private double balance;
private final ReentrantLock lock = new ReentrantLock();
private final Condition sufficientFunds = lock.newCondition();
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void withdraw(double amount) {
lock.lock();
try {
while (balance < amount) {
sufficientFunds.await(); // 等待资金充足
}
balance -= amount;
System.out.println("Withdraw: " + amount + ", Balance: " + balance);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
public void deposit(double amount) {
lock.lock();
try {
balance += amount;
System.out.println("Deposit: " + amount + ", Balance: " + balance);
sufficientFunds.signalAll(); // 唤醒所有等待线程
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
BankAccount account = new BankAccount(1000);
new Thread(() -> {
for (int i = 0; i < 5; i++) {
account.withdraw(300);
}
}).start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
account.deposit(500);
}
}).start();
}
}
今天我们学习了 Java 中三种重要的线程间通信机制:
wait/notify
:经典的线程通信方式,适用于简单同步场景。Condition
:基于 ReentrantLock
的高级条件控制机制,提供更灵活的线程协作方式。CountDownLatch
:用于控制多个线程的启动或结束,适合一次性事件触发。这些技术广泛应用于生产者-消费者模型、线程池调度、任务编排等实际开发场景。理解它们的底层实现原理和性能差异,有助于我们在高并发环境中做出更优的设计决策。
明天我们将深入探讨线程池的原理与使用技巧,包括 ThreadPoolExecutor
的参数调优、拒绝策略、自定义线程工厂等内容,敬请期待!