Java 锁重入与条件变量在 ReentrantLock 中的实现
Java 提供了 ReentrantLock 类作为内置锁(synchronized)的替代方案,它支持更灵活的锁控制。其中两个核心特性是锁重入和条件变量。理解它们的实现机制,能帮助你写出更高效、安全的并发代码。
什么是锁重入?
锁重入是指同一个线程可以多次获取同一把锁而不会发生死锁。例如,一个方法 A 调用了另一个也使用相同锁的方法 B,只要这两个方法由同一线程执行,就能顺利运行。
验证锁重入能力:
- 创建一个
ReentrantLock实例。 - 调用
lock()方法两次(在同一线程中)。 - 确保最终调用两次
unlock()才能完全释放锁。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantExample {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
System.out.println("Outer locked");
inner(); // 同一线程再次获取锁
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock();
try {
System.out.println("Inner locked");
} finally {
lock.unlock();
}
}
}
上述代码能正常运行,说明 ReentrantLock 支持重入。
ReentrantLock 如何实现锁重入?
ReentrantLock 内部通过一个计数器(state)和持有线程引用来实现重入:
- 当线程首次获取锁时,
state设为 1,并记录当前线程。 - 若同一线程再次调用
lock(),state自增。 - 每次调用
unlock(),state自减。 - 只有当
state回到 0 时,锁才真正释放,其他线程才有机会获取。
这个机制依赖于 AbstractQueuedSynchronizer(AQS),它是 Java 并发包的核心同步框架。
条件变量是什么?为什么需要它?
条件变量允许线程在某个条件不满足时挂起等待,直到其他线程通知条件已满足。这比轮询(busy-waiting)更节省 CPU 资源。
在 ReentrantLock 中,通过 newCondition() 方法创建条件变量,返回一个 Condition 对象。
典型使用场景:生产者-消费者模型。
使用 Condition 实现线程协作
实现一个有界缓冲区,支持生产者写入、消费者读取,并在线程安全的前提下阻塞等待:
- 定义一个固定大小的数组作为缓冲区。
- 创建一个
ReentrantLock和一个关联的Condition(如notFull和notEmpty)。 - 生产者在缓冲区满时调用
await()等待。 - 消费者在缓冲区空时调用
await()等待。 - 每次操作后,根据状态调用
signal()或signalAll()唤醒等待线程。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer {
final Object[] items = new Object[10];
int putIndex, takeIndex, count;
final ReentrantLock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 缓冲区满,等待
}
items[putIndex] = x;
if (++putIndex == items.length) putIndex = 0;
++count;
notEmpty.signal(); // 通知消费者可以取了
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 缓冲区空,等待
}
Object x = items[takeIndex];
if (++takeIndex == items.length) takeIndex = 0;
--count;
notFull.signal(); // 通知生产者可以放了
return x;
} finally {
lock.unlock();
}
}
}
关键点:
- 必须在持有锁的情况下调用
await()或signal(),否则会抛出IllegalMonitorStateException。 - 使用
while而非if判断条件,防止虚假唤醒(spurious wakeup)。
Condition 的内部机制
每个 Condition 对象内部维护一个等待队列(不同于 AQS 的同步队列)。调用 await() 时:
- 释放当前持有的锁(自动调用
unlock效果)。 - 将当前线程封装成节点,加入该
Condition的等待队列。 - 阻塞线程,直到被
signal()唤醒。 - 被唤醒后,重新竞争锁,成功获取后继续执行。
调用 signal() 时:
- 从等待队列中取出第一个节点。
- 将其移入 AQS 的同步队列,参与锁的竞争。
这种设计实现了“等待-通知”机制与锁的解耦,允许多个条件变量共享同一把锁。
锁重入与 Condition 的交互
由于 ReentrantLock 支持重入,同一个线程可以在持有锁的情况下多次调用 lock(),但调用 await() 会完全释放锁(释放所有重入次数),并在被唤醒后重新获取全部重入次数。
例如:
- 线程 T 已重入 3 次(
state = 3)。 - T 调用
condition.await()→state变为 0,T 进入等待队列。 - 其他线程可获取锁。
- 当 T 被
signal()唤醒并重新获得锁后,state恢复为 3。
这一行为保证了语义一致性:await() 前后,线程对锁的“持有深度”不变。
常见错误与最佳实践
| 错误做法 | 正确做法 |
|---|---|
在 synchronized 块中使用 Condition |
只在 ReentrantLock 保护下使用 Condition |
用 if 判断条件后调用 await() |
始终用 while 循环检查条件 |
忘记在 finally 块中 unlock() |
lock() 后必须配对 unlock(),且放在 finally 中 |
调用 signal() 后不释放锁(长时间持有) |
尽快释放锁,避免阻塞其他线程 |
性能与适用场景
- 高竞争场景:
ReentrantLock提供公平锁选项(构造时传true),可减少线程饥饿,但吞吐量略低。 - 需要超时或中断:
ReentrantLock支持tryLock(timeout, unit)和可中断的lockInterruptibly(),比synchronized更灵活。 - 多条件等待:若需多个等待条件(如“队列满”、“队列空”、“任务完成”),
Condition比单个wait/notify更清晰。
选择建议:
- 简单同步 → 用
synchronized。 - 需要高级功能(超时、多条件、公平性)→ 用
ReentrantLock+Condition。
验证你的理解:动手测试
编写一个测试用例,验证重入和条件变量协同工作:
- 启动两个线程:一个生产者、一个消费者。
- 生产者连续放入 5 个元素。
- 消费者逐个取出并打印。
- 确保程序不卡死、无异常、输出顺序正确。
public class TestBoundedBuffer {
public static void main(String[] args) throws InterruptedException {
BoundedBuffer buffer = new BoundedBuffer();
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
buffer.put("Item-" + i);
System.out.println("Produced: Item-" + i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
Object item = buffer.take();
System.out.println("Consumed: " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
运行结果应交替输出“Produced”和“Consumed”,证明锁重入和条件变量正常工作。

暂无评论,快来抢沙发吧!