在Java并发编程中,synchronized与ReentrantLock的选择与对比是一个高频话题。本文将用生动的比喻为你解析它们的核心区别,帮助你从容应对面试,并在实战中做出正确选择。
synchronized vs ReentrantLock:一场武侠世界的锁之争
身世来历:嫡传弟子 vs 宗门高手
Synchronized和ReentrantLock,就好比武林中的两位顶尖高手,但他们的出身完全不同。
Synchronized,我们称之为 “少林锁僧” 。他是Java这门“武林”的嫡传弟子,从1995年Java诞生起,就是这门语言的内功心法之一。他的武功招式直接刻在Java的“武学典籍”(JVM规范)里。
而ReentrantLock,我们称之为 “华山剑客” 。他不是语言本身的招式,而是后来加入的门派高手。他来自Java 5时代成立的“并发宗门”(java.util.concurrent包)。这个宗门专门研究多线程、高并发这些现代武学难题。
根本区别:“少林锁僧”的武功是天生的,内置于语言层面;“华山剑客”的武功是后天修炼的,位于API层面。
练功方式:自动流转 vs 手动操控
“少林锁僧”synchronized,修炼的是“自动流转功”。 其用法特别简单,例如:
public synchronized void 取经() {
// 一次只让一个线程进入
翻阅秘籍();
}
或者:
public void 取经() {
synchronized(藏经阁大门) {
// 锁住大门,别人进不来
翻阅秘籍();
}
// 自动解锁,下个人可以进
}
最大的特点是:锁自动获取,自动释放。 你进入同步代码块,门自动锁上;你出来,门自动打开。即使抛出异常,锁也会被自动释放,避免了死锁风险。
“华山剑客”ReentrantLock,修炼的是“手动操控诀”。 其使用方式如下:
private final ReentrantLock 思过崖大门 = new ReentrantLock();
public void 面壁() {
思过崖大门.lock(); // 手动锁门
try {
面壁思过();
} finally {
思过崖大门.unlock(); // 必须手动开门
}
}
关键区别在于必须显式调用 unlock()。 “华山剑客”的规矩是:无论临界区内执行成功与否,在 finally 块中必须亲手解锁。如果遗忘,将导致死锁。
武功招式:朴实无华 vs 丰富多彩
“少林锁僧”synchronized,招式朴实,但基本功扎实。 核心特性就两点:
- 可重入:同一个线程可重复获取同一把锁。
- 互斥:同一时间只允许一个线程进入。
对于大多数并发场景,这已经足够。但“华山剑客”ReentrantLock来自专攻高并发的“并发宗门”,其招式则丰富得多:
第一式:试探敲门手(tryLock)
if (思过崖大门.tryLock()) { // 尝试获取,失败立即返回
try {
进去练功();
} finally {
思过崖大门.unlock();
}
} else {
去别处转转(); // 不傻等
}
更有带时限的试探:
// 只等待3秒,超时则放弃
if (思过崖大门.tryLock(3, TimeUnit.SECONDS)) {
// ...
}
第二式:知难而退功(可中断锁)
使用 lockInterruptibly() 方法,等待锁的线程可以被其他线程中断,避免无限期等待。
try {
思过崖大门.lockInterruptibly(); // 可以被打断的等待
// ...
} catch (InterruptedException e) {
// 被中断,处理其他事务
}
第三式:公平对决令(公平锁)
“少林锁僧”的锁默认是非公平的(竞争机制,性能高)。而“华山剑客”提供了公平性的选择:
// 公平锁:严格按照线程等待顺序获取锁
ReentrantLock 公平大门 = new ReentrantLock(true);
// 非公平锁:默认模式,谁抢到谁进
ReentrantLock 非公平大门 = new ReentrantLock();
第四式:多重等待室(多个Condition)
这是ReentrantLock的招牌绝技。synchronized只关联一个隐式的等待队列(wait/notifyAll);而ReentrantLock可以创建多个Condition对象,实现更精细的线程通信。
Lock lock = new ReentrantLock();
Condition 等上房 = lock.newCondition(); // 等待豪华房队列
Condition 等通铺 = lock.newCondition(); // 等待普通床位队列
// 掌柜可以精准通知特定队列
上房空出来了();
等上房.signal(); // 只唤醒一个等待上房的线程
// 或
通铺空出来了();
等通铺.signalAll(); // 唤醒所有等待通铺的线程
实战选型:没有最强,只有最合适
优先选择“少林锁僧”synchronized的场景:
- 同步逻辑简单直接。
- 不需要尝试获取、定时获取或可中断获取锁等高级功能。
- 不想操心锁的释放管理(依赖于自动释放)。
- 团队技术栈统一,维护简单。
// 简单的计数器 - synchronized足够简洁安全
public class Counter {
private int count;
public synchronized void increment() {
count++;
}
}
考虑选择“华山剑客”ReentrantLock的场景:
- 需要尝试获取锁(
tryLock),避免死锁或长时间等待。
- 需要可中断的锁获取机制。
- 需要公平锁保证顺序。
- 需要复杂的线程间协调(多个
Condition)。
- 需要实现类似“手递手”的精细锁控制逻辑。
例如,在银行转账这种容易死锁的场景,tryLock就非常有用:
public class BankTransfer {
private Map<String, ReentrantLock> accountLocks = new HashMap<>();
public boolean transfer(String from, String to, int amount) {
// 尝试获取第一把锁
if (accountLocks.get(from).tryLock()) {
try {
// 尝试获取第二把锁
if (accountLocks.get(to).tryLock()) {
try {
// 执行转账
return true;
} finally {
accountLocks.get(to).unlock();
}
}
} finally {
accountLocks.get(from).unlock();
}
}
return false; // 获取锁失败,可重试或回退
}
}
关于性能的“江湖传说”
这是一个常见的误解。在Java 6之前,synchronized作为重量级锁性能确实较差。但Java 6及之后,JVM团队对synchronized进行了重大优化,引入了锁升级机制:
无锁状态 → 偏向锁 → 轻量级锁 → 重量级锁
这意味着,在低竞争情况下,synchronized的性能开销非常小。只有在高竞争场景下,它才会膨胀为重量级锁。因此,在绝大多数应用场景中,两者的性能差异并不明显,不应作为选型的首要依据。
江湖铁律
记住这条核心原则:能用synchronized解决的,就优先使用synchronized。只有当synchronized的功能无法满足需求时,才考虑使用功能更强大但也更复杂的ReentrantLock。
一个恰当的比喻是:synchronized如同自动挡汽车,易于驾驶,安全性高;ReentrantLock如同手动挡汽车,控制精细,性能潜力大,但需要更谨慎的操作,否则易“熄火”(死锁)。深入理解Java并发工具箱中的这些核心组件,是构建健壮高并发应用的基础。