Java 显式锁与 Condition 的使用详解
在多线程编程中,线程间的协作与同步是核心问题。Java 提供了多种机制来实现线程同步,除了传统的 synchronized
关键字外,ReentrantLock
和 Condition
是更灵活且功能强大的替代方案。本文将详细介绍显式锁与 Condition
的使用,并通过实际案例分析其工作原理及常见误区。
一、显式锁与 Condition 简介
1. 显式锁(ReentrantLock)
ReentrantLock
是 Java 提供的可重入互斥锁,它比 synchronized
提供了更高的灵活性:
- 支持尝试加锁(
tryLock()
)。 - 支持超时等待。
- 支持公平锁与非公平锁。
- 可以绑定多个
Condition
条件变量。
2. Condition 条件变量
Condition
是与 ReentrantLock
绑定的条件队列,用于实现线程的等待和唤醒。它替代了传统的 Object.wait()
和 Object.notify()
,并且支持为不同的条件定义独立的等待队列,从而更精确地控制线程协作。
二、典型使用场景:生产者-消费者模型
以下是一个使用 ReentrantLock
和 Condition
实现的**有界缓冲区(Bounded Buffer)**示例:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer<T> {
private final List<T> buffer;
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public BoundedBuffer(int capacity) {
this.capacity = capacity;
this.buffer = new ArrayList<>();
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (buffer.size() == capacity) {
notFull.await(); // 缓冲区满时等待
}
buffer.add(item);
notEmpty.signal(); // 通知消费者缓冲区非空
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (buffer.isEmpty()) {
notEmpty.await(); // 缓冲区空时等待
}
T item = buffer.remove(0);
notFull.signal(); // 通知生产者缓冲区有空间
return item;
} finally {
lock.unlock();
}
}
}
主程序:启动生产者与消费者线程
public class ProducerConsumerExample {
public static void main(String[] args) {
BoundedBuffer<Integer> buffer = new BoundedBuffer<>(10);
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
buffer.put(i);
System.out.println("Produced: " + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
Integer item = buffer.take();
System.out.println("Consumed: " + item);
Thread.sleep(150);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
示例说明
ReentrantLock
:通过lock.lock()
和lock.unlock()
控制对共享资源的访问。Condition
:notFull
和notEmpty
分别用于生产者和消费者线程的等待与唤醒。while
循环:用于防止虚假唤醒(Spurious Wakeup),确保条件满足后再继续执行。
三、常见误区:不加锁调用 Condition.await()
❌ 错误示例
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 未加锁直接调用 await()
condition.await(); // 抛出 IllegalMonitorStateException
✅ 正确做法
必须在持有锁的情况下调用 await()
:
lock.lock();
try {
while (!conditionMet) {
condition.await(); // 必须持有锁
}
} finally {
lock.unlock();
}
🔒 为什么必须加锁?
-
原子性释放锁与等待:
await()
的设计要求线程在调用时持有锁,以便在等待前释放锁,防止死锁。- 如果未持有锁,线程无法确定要释放哪个锁,导致逻辑混乱。
-
Java 的强制性检查:
Condition
内部会检查当前线程是否持有锁。若未持有,会抛出IllegalMonitorStateException
。
-
与 Object.wait() 的类比:
Object.wait()
必须在synchronized
块中调用。Condition.await()
必须在ReentrantLock.lock()
保护的代码块中调用。
四、最佳实践
-
始终使用
while
而不是if
:- 防止虚假唤醒(Spurious Wakeup),确保条件再次检查。
-
在 finally 块中释放锁:
- 避免因异常导致锁未释放,造成死锁。
-
选择 signal 或 signalAll:
signal()
唤醒单个线程,适用于单一条件满足的场景。signalAll()
唤醒所有等待线程,适用于复杂条件变化的场景。
-
避免在无锁状态下操作 Condition:
- 所有
await()
、signal()
操作必须在持有锁的上下文中执行。
- 所有
五、总结
ReentrantLock
和 Condition
是 Java 多线程编程中强大的工具,它们提供了比 synchronized
更细粒度的线程控制能力。通过合理使用锁和条件变量,可以高效实现线程协作,避免竞态条件和死锁问题。但需注意:
- 必须在持有锁的情况下调用
await()
和signal()
。 - 使用
while
循环检查条件。 - 始终在
finally
块中释放锁。
掌握这些原则,能够帮助开发者构建出安全、高效的并发程序。