Java 中 synchronized 和 Lock 该怎么选?
Java 中短小互斥且无需超时、中断时用 synchronized;需要 tryLock、超时、可中断、公平锁、多 Condition、读写分离或 JDK 21~23 虚拟线程时用 ReentrantLock。
学完这篇,你能拿到一份可执行的判断清单:面对一段 Java 并发代码时,知道该用 synchronized 还是 java.util.concurrent.locks.Lock(实际几乎都是 ReentrantLock),以及写完怎么验证没选错。
第一步:认清两者的定位
这一步要弄清它们分别是什么、从哪个版本开始可用,结果是你不会再把它俩当成「性能高低二选一」。
synchronized是关键字,由 JVM 实现,JDK 1.0 就有。它锁定的是对象监视器(monitor)。Lock是接口,位于java.util.concurrent.locks包,JDK 5 引入。日常用的是实现类ReentrantLock。- 锁优化(偏向锁、轻量级锁)从 JDK 6 开始进入
synchronized,所以「synchronized 一定慢」是 JDK 5 时代的旧结论。偏向锁在 JDK 15 起默认禁用(JEP 374),不用再纠结它。
注意:JDK 21 的虚拟线程里,
synchronized块会把载体线程 pin 住,可能拖垮吞吐;JDK 24(JEP 491)已解除该限制。如果你在 JDK 21~23 上跑虚拟线程,优先用ReentrantLock。
第二步:把差异列成对照表
这一步把选型依据固定下来,结果是你能对着表逐条打勾。
| 能力 | synchronized | ReentrantLock |
|---|---|---|
| 获取方式 | 进入代码块自动获取 | 手动 lock() |
| 释放方式 | 退出代码块自动释放 | 必须 finally 里 unlock() |
| 尝试获取不阻塞 | 不支持 | tryLock() |
| 带超时获取 | 不支持 | tryLock(long, TimeUnit) |
| 可中断获取 | 不支持 | lockInterruptibly() |
| 公平锁 | 不支持 | new ReentrantLock(true) |
| 多个条件队列 | 一个(wait/notify) | 多个 Condition |
| 读写分离 | 不支持 | ReentrantReadWriteLock |
第三步:按场景做选择
这一步是决策动作,结果是你直接得到答案。
满足下面任意一条,选 ReentrantLock:
- 需要「拿不到锁就先干别的」:调
tryLock(),返回false就跳过。 - 需要超时:
lock.tryLock(3, TimeUnit.SECONDS),超时走降级逻辑。 - 需要可取消:
lockInterruptibly(),线程被interrupt()时抛InterruptedException。 - 需要精确唤醒:生产者消费者用两个
Condition,notFull.await()和notEmpty.signal()分开。 - 需要公平锁:
new ReentrantLock(true),按等待顺序发放。 - 读多写少:用
ReentrantReadWriteLock,读锁共享、写锁互斥。
其余情况——只要一段短小的互斥逻辑、不需要超时和中断——直接用 synchronized,代码短、不会忘解锁。
第四步:写两段等价代码对照
这一步把模板抄下来,结果是你能马上替换现有代码。
// 方式一:synchronized
public synchronized void add(int n) {
this.count += n;
}
// 方式二:ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
public void add(int n) {
lock.lock();
try {
this.count += n;
} finally {
lock.unlock();
}
}
lock() 放在 try 外面第一行,unlock() 放 finally,这两条不能改。
注意:
unlock()千万别写在try内部结尾,一旦中间抛异常锁就永远不释放。也不要吞掉InterruptedException,至少要Thread.currentThread().interrupt()还原中断标记。
第五步:验证你的选择
这一步确认线上不会卡死,结果是你能定位问题。
- 怀疑死锁:命令行执行
jstack <pid>,或jcmd <pid> Thread.print,搜waiting to lock和parking to wait for。 - 想看锁竞争:启动时加
-XX:+UnlockDiagnosticVMOptions,用 JFR 录制jcmd <pid> JFR.start duration=60s filename=lock.jfr。 - 做性能对比:用 JMH,别用
System.currentTimeMillis()手写循环,JIT 会把你的测量结果优化掉。 - 公平锁一定要测吞吐,
new ReentrantLock(true)通常比非公平慢,只在确实需要避免饥饿时开。
小结
- 默认先用
synchronized,短小互斥它更安全,不会忘unlock。 - 需要
tryLock、超时、可中断、多个Condition、公平锁、读写分离时,换ReentrantLock。 ReentrantLock的固定模板是lock()在try外、unlock()在finally里。- JDK 21~23 跑虚拟线程时避开
synchronized长临界区,JDK 24 起此限制已解除。 - 选完用
jstack和 JMH 验证,不要凭感觉。
原文链接:https://www.gj0.com/thread-1678.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。