ReentrantLock 与 Condition
学习目标
- 正确掌握
lock、unlock、tryLock和lockInterruptibly的生命周期。 - 使用 Condition 把不同等待条件分到不同队列。
- 知道公平锁、读写锁和 StampedLock 的选择边界。
核心知识点
专业术语
- ReentrantLock:可重入的显式互斥锁,提供 synchronized 没有的可中断、超时和公平选项。
- Condition:绑定到 Lock 的条件队列,作用类似 wait/notify,但可为不同条件分别建队列。
- 公平锁(fair lock):倾向按等待时间顺序授予锁,减少插队但通常吞吐更低。
- ReadWriteLock:把读锁和写锁分开,允许多个读者并行、写者独占。
- StampedLock:支持乐观读的低频锁工具,不可重入。
白话解释与边界
显式锁的能力更强,责任也更大:成功获得锁后必须在 finally 中解锁;等待、超时和异常路径都要设计。Condition 的 await 会释放绑定的 Lock,signal 只是唤醒候选线程,线程仍需重新获得锁并检查条件。
公平锁不是“绝对没有饥饿”的性能保证;大多数短临界区先用默认非公平锁,只有调度体验和尾延迟有证据时才选择公平策略。
常用用法
使用显式锁:获得后在 finally 释放
lock() 返回后代表当前线程已经持有锁;即使临界区抛异常也必须释放。
// 说明:使用显式锁:获得后在 finally 释放。
import java.util.concurrent.locks.ReentrantLock;
public class LockFinallyDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
// 初始状态:lock = new ReentrantLock()。
// 作用:ReentrantLock lock = new ReentrantLock();;获得后在 finally 释放。
lock.lock();
// 初始状态:lock.lock()。
// 作用:lock.lock();;获得后在 finally 释放。
try {
System.out.println("inside lock");
// 输出:inside lock
} finally {
lock.unlock();
// 初始状态:lock.unlock()。
// 作用:lock.unlock();;获得后在 finally 释放。
}
}
}不要把 unlock 放在可能未成功加锁的路径上。
tryLock:有界地尝试获得锁
tryLock 能避免无限等待,适合降级、重试或按锁顺序获取;超时分支必须有业务策略,不能静默丢请求。
// 说明:tryLock:有界地尝试获得锁。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockDemo {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
// 初始状态:lock = new ReentrantLock()。
// 作用:ReentrantLock lock = new ReentrantLock();;有界地尝试获得锁。
if (lock.tryLock(10, TimeUnit.MILLISECONDS)) {
// 作用:if (lock.tryLock(10, TimeUnit.MILLISECONDS)) {;有界地尝试获得锁。
try {
System.out.println("acquired");
// 输出:acquired
} finally {
lock.unlock();
// 作用:lock.unlock();;有界地尝试获得锁。
}
} else {
System.out.println("busy");
// 输出:可能是 busy(如果超时未获得)
}
}
}lockInterruptibly:可响应中断地等待锁
与不可中断的 lock() 相比,它允许上层取消等待;捕获 InterruptedException 后应决定退出、恢复中断或转成业务异常。
// 说明:lockInterruptibly:可响应中断地等待锁。
import java.util.concurrent.locks.ReentrantLock;
public class InterruptibleLockDemo {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
// 作用:ReentrantLock lock = new ReentrantLock();;可响应中断地等待锁。
// 初始状态:lock = new ReentrantLock()。
lock.lockInterruptibly();
// 初始状态:lock.lockInterruptibly()。
// 作用:lock.lockInterruptibly();;可响应中断地等待锁。
try {
System.out.println("interruptible lock");
// 输出:interruptible lock
} finally {
lock.unlock();
// 作用:lock.unlock();;可响应中断地等待锁。
}
}
}等待锁条件:释放锁并广播唤醒
await 必须在持有绑定 Lock 时调用;醒来后要 while 检查条件。
// 说明:等待锁条件:释放锁并广播唤醒。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
// 初始状态:lock = new ReentrantLock()。
// 作用:private final ReentrantLock lock = new ReentrantLock();;释放锁并广播唤醒。
private final Condition ready = lock.newCondition();
// 初始状态:ready = lock.newCondition()。
// 作用:private final Condition ready = lock.newCondition();;释放锁并广播唤醒。
private boolean available;
void awaitReady() throws InterruptedException {
lock.lock();
// 初始状态:lock.lock()。
// 作用:lock.lock();;释放锁并广播唤醒。
try {
while (!available) {
ready.await();
// 初始状态:ready.await()。
// 作用:ready.await();;释放锁并广播唤醒。
}
} finally {
lock.unlock();
// 初始状态:lock.unlock()。
// 作用:lock.unlock();;释放锁并广播唤醒。
}
}
void markReady() {
lock.lock();
// 初始状态:lock.lock()。
// 作用:lock.lock();;释放锁并广播唤醒。
try {
available = true;
// 初始状态:available = true。
ready.signalAll();
// 初始状态:ready.signalAll()。
// 作用:ready.signalAll();;释放锁并广播唤醒。
} finally {
lock.unlock();
// 初始状态:lock.unlock()。
// 作用:lock.unlock();;释放锁并广播唤醒。
}
}
public static void main(String[] args) throws InterruptedException {
var state = new ConditionDemo();
// 初始状态:state = new ConditionDemo()。
// 作用:var state = new ConditionDemo();;释放锁并广播唤醒。
state.markReady();
// 初始状态:state.markReady()。
// 作用:state.markReady();;释放锁并广播唤醒。
state.awaitReady();
// 初始状态:state.awaitReady()。
// 作用:state.awaitReady();;释放锁并广播唤醒。
System.out.println("ready");
// 输出:ready
}
}多个 Condition 可以分别表示 notEmpty、notFull 等状态,减少无关线程唤醒。
ReentrantLock(boolean fair):创建公平锁
公平锁减少插队机会,但会增加排队和调度成本;默认非公平锁往往吞吐更好,先用指标证明需要公平性。
// 说明:ReentrantLock(boolean fair):创建公平锁。
import java.util.concurrent.locks.ReentrantLock;
public class FairLockDemo {
public static void main(String[] args) {
ReentrantLock fair = new ReentrantLock(true);
// 初始状态:fair = new ReentrantLock(true)。
// 作用:ReentrantLock fair = new ReentrantLock(true);;创建公平锁。
fair.lock();
// 初始状态:fair.lock()。
// 作用:fair.lock();;创建公平锁。
try {
System.out.println("fair lock configured=" + fair.isFair());
// 输出:fair lock configured=true
} finally {
fair.unlock();
// 初始状态:fair.unlock()。
// 作用:fair.unlock();;创建公平锁。
}
}
}getHoldCount:查看当前线程的重入次数
重入次数只用于诊断或断言,不能作为业务状态。
// 说明:getHoldCount:查看当前线程的重入次数。
import java.util.concurrent.locks.ReentrantLock;
public class HoldCountDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
// 初始状态:lock = new ReentrantLock()。
// 作用:ReentrantLock lock = new ReentrantLock();;查看当前线程的重入次数。
lock.lock();
// 作用:lock.lock();;查看当前线程的重入次数。
try {
lock.lock();
// 作用:lock.lock();;查看当前线程的重入次数。
try {
System.out.println(lock.getHoldCount());
// 输出:2
} finally {
lock.unlock();
// 作用:lock.unlock();;查看当前线程的重入次数。
}
} finally {
lock.unlock();
// 作用:lock.unlock();;查看当前线程的重入次数。
}
}
}每次成功 lock 都必须对应一次 unlock。
不常用但需要知道
Condition.awaitNanos:带剩余时间的等待
超时等待可能被提前 signal 或中断,返回值只是剩余时间提示。业务条件仍需要在循环中检查,不应只依据返回值判定成功。
// 作用:通过 Condition.awaitNanos 带剩余时间的等待。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionTimeoutDemo {
public static void main(String[] args) throws InterruptedException {
var lock = new ReentrantLock();
// 作用:var lock = new ReentrantLock();;带剩余时间的等待。
// 初始状态:lock = new ReentrantLock()。
var condition = lock.newCondition();
// 作用:var condition = lock.newCondition();;带剩余时间的等待。
// 初始状态:condition = lock.newCondition()。
lock.lock();
// 作用:lock.lock();;带剩余时间的等待。
try {
long remaining = condition.awaitNanos(TimeUnit.MILLISECONDS.toNanos(1));
// 初始状态:remaining = condition.awaitNanos(TimeUnit.MILLISECONDS.toNanos(1))。
// 作用:long remaining = condition.awaitNanos(TimeUnit.MILLISECONDS.toNanos(1));;带剩余时间的等待。
System.out.println("timed wait finished=" + (remaining <= 0));
// 输出:通常是 timed wait finished=true;也可能是 false(提前唤醒)
} finally {
lock.unlock();
// 作用:lock.unlock();;带剩余时间的等待。
}
}
}ReentrantReadWriteLock:读写锁
读锁并行不等于一定更快;写频繁、读临界区很短或升级路径复杂时,普通锁可能更清晰。不要在持有读锁时直接申请写锁形成升级死锁。
// 说明:当前线程获得 lock.readLock() 后只读共享快照,finally 释放读锁,避免后续写线程一直被阻塞。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockLowFrequencyDemo {
public static void main(String[] args) {
var lock = new ReentrantReadWriteLock();
// 初始状态:lock = new ReentrantReadWriteLock()。
// 作用:var lock = new ReentrantReadWriteLock();;读写锁。
lock.readLock().lock();
// 作用:lock.readLock().lock();;读写锁。
try {
System.out.println("shared read");
// 输出:shared read
} finally {
lock.readLock().unlock();
// 作用:lock.readLock().unlock();;读写锁。
}
}
}StampedLock:乐观读并校验
StampedLock 不可重入,乐观读必须 validate,失败后回退到读锁。只有读多写少且基准显示收益时才使用。
// 说明:tryOptimisticRead() 产生 stamp;若 validate(stamp) 发现期间有写锁,就持有 readLock 重读 result。
import java.util.concurrent.locks.StampedLock;
public class StampedLockLowFrequencyDemo {
public static void main(String[] args) {
StampedLock lock = new StampedLock();
// 初始状态:lock = new StampedLock()。
// 作用:StampedLock lock = new StampedLock();;乐观读并校验。
long stamp = lock.tryOptimisticRead();
// 作用:long stamp = lock.tryOptimisticRead();;乐观读并校验。
// 初始状态:stamp = lock.tryOptimisticRead()。
int result = 7;
// 初始状态:result = 7。
if (!lock.validate(stamp)) {
stamp = lock.readLock();
// 作用:stamp = lock.readLock();;乐观读并校验。
// 初始状态:stamp = lock.readLock()。
try {
result = 7;
// 初始状态:result = 7。
} finally {
lock.unlockRead(stamp);
// 作用:lock.unlockRead(stamp);;乐观读并校验。
}
}
System.out.println(result);
// 输出:7
}
}简单案例
import java.util.concurrent.locks.ReentrantLock;
public class LockCounterDemo {
private final ReentrantLock lock = new ReentrantLock();
// 初始状态:lock = new ReentrantLock()。
// 作用:private final ReentrantLock lock = new ReentrantLock();;乐观读并校验。
private int count;
void increment() {
lock.lock();
// 作用:lock.lock();;乐观读并校验。
try {
count++;
} finally {
lock.unlock();
// 作用:lock.unlock();;乐观读并校验。
}
}
int count() {
lock.lock();
// 作用:lock.lock();;乐观读并校验。
try {
return count;
} finally {
lock.unlock();
// 作用:lock.unlock();;乐观读并校验。
}
}
public static void main(String[] args) {
var counter = new LockCounterDemo();
// 作用:var counter = new LockCounterDemo();;乐观读并校验。
// 初始状态:counter = new LockCounterDemo()。
counter.increment();
// 作用:counter.increment();;乐观读并校验。
counter.increment();
// 作用:counter.increment();;乐观读并校验。
System.out.println(counter.count());
// 输出:2
}
}把每次成功的 lock 放在 try 之前、unlock 放在 finally 中,是显式锁最重要的固定模板。
易混点
tryLock返回 false 时没有持锁,不能无条件 unlock。- Condition 属于某个 Lock;await、signal 和 signalAll 都必须在持有该 Lock 时调用。
signal()只是通知,不代表条件成立,也不保证被唤醒线程马上运行。- 公平锁只改变获取策略,不修复临界区过大、锁顺序错误或外部 I/O 阻塞。
- StampedLock 不可重入,读锁升级和乐观读校验都比 ReentrantLock 更容易误用。
课后小问
为什么 unlock 必须写在 finally? 答案:临界区任何 return 或异常都不能跳过释放,否则后续线程可能永久阻塞。 解析:只有成功获得锁后才进入 try;tryLock 失败不能执行对应 unlock。
Condition.signal 后被唤醒的线程会立即执行吗? 答案:不会,它还要重新获得关联的 Lock,并且重新检查等待条件。 解析:signal 只把线程从条件队列移到竞争队列,锁的所有权仍由当前线程持有到退出临界区。
公平锁一定比非公平锁更好吗? 答案:不一定,公平性通常降低吞吐,只有饥饿或尾延迟目标明确时才值得付成本。 解析:先用压测观察排队、吞吐和延迟,再选择公平参数。
本节小结
- ReentrantLock 提供显式解锁、可中断、超时和公平策略。
- Condition 用独立条件队列实现生产/消费等等待协议,必须 while 检查状态。
- 读写锁和 StampedLock 是低频优化工具,先验证读写比例与性能收益。
- tryLock、超时和锁顺序可以减少无限等待,但都需要明确失败策略。
快速回顾
- 会写 lock/try/finally/unlock 模板。
- 能区分 tryLock、lockInterruptibly 和 lock 的等待语义。
- 会使用 Condition.await/signalAll 协调条件状态。
- 知道公平锁、读写锁和 StampedLock 的适用边界。
阿源的知识库