什么时候轮到它出场
第 10 篇看过它的内脏,本篇说使用。先立选型观:默认场景用 synchronized——语法简洁、自动放锁、JVM 还在持续优化;只有需要下面四样能力之一时,才换 ReentrantLock。
能力一:可中断的等待
synchronized 等锁时对 interrupt 无感——线程挂在 BLOCKED 里纹丝不动,真死锁了只能重启进程。而 lockInterruptibly() 在等锁途中收到中断会抛 InterruptedException、立即退出排队。这是死锁恢复的正规解法之一:检测到死锁后,中断某个持锁线程打破僵局。
能力二:可超时的获取
tryLock() 无参版立刻返回,拿到拿不到都走人,不排队;带时间版等一会儿就撤。典型用法:抢锁失败就放弃或降级——记日志、走备用路径,「等不起」的后果由业务自己定义。缓存重建、防重复任务这类场景极好用:第二个线程发现有人在重建,转身就走,不傻等。
能力三:公平锁
构造时 new ReentrantLock(true) 开公平模式,严格按排队顺序发锁——呼应第 10 篇,差异只在要不要先看队列。但默认非公平是对的:公平锁吞吐明显更低,除非确认存在饿死且业务不可接受,否则别开。
能力四:多个条件队列 Condition
synchronized 只有一套等待队列,wait/notify 一锅粥,notify 还可能喊错人(第 4 篇的死锁案例)。ReentrantLock 可以 new 出多个 Condition,把不同目的的等待分组,signal 定点喊醒。用 Condition 重写第 4 篇的缓冲区:
class Buffer {
private final Queue<Integer> data = new LinkedList<>();
private final int cap = 10;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 生产者的等待组
private final Condition notEmpty = lock.newCondition(); // 消费者的等待组
public void put(int v) throws InterruptedException {
lock.lock();
try {
while (data.size() == cap) notFull.await(); // 满了,去生产者组等
data.offer(v);
notEmpty.signal(); // 定点唤醒消费者
} finally {
lock.unlock(); // 必须 finally 释放
}
}
}await 释放锁、signal 唤醒,语义对齐 wait/notify,但等待者分了组:notFull 上只挂生产者、notEmpty 上只挂消费者,signal 永不误伤。代价是纪律更严:await/signal 同样必须持锁调用;unlock 忘在 finally 里,异常一抛锁就永远还不回去——synchronized 自动放锁的省心,换成了这份自律要求。
最后提醒一句:ReentrantLock 的加解锁可以跨方法使用——灵活是双刃剑,lock 与 unlock 必须严格配对。选型检查清单:要可中断?要可超时?要公平?要多条件?四个都不要,回 synchronized。下一篇读写锁——读多写少的场景,独占太浪费了。
评论 (0)