注册

⚔️ ReentrantLock大战synchronized:谁是锁界王者?

一、选手登场!🎬


🔵 蓝方:synchronized(老牌选手)


// synchronized:Java自带的语法糖
public synchronized void method() {
// 临界区代码
}

// 或者
public void method() {
synchronized(this) {
// 临界区代码
}
}

特点:



  • 📜 JDK 1.0就有了,资历老
  • 🎯 简单粗暴,写法简单
  • 🤖 JVM级别实现,自动释放
  • 💰 免费午餐,不需要手动管理

🔴 红方:ReentrantLock(新锐选手)


// ReentrantLock:JDK 1.5引入
ReentrantLock lock = new ReentrantLock();

public void method() {
lock.lock(); // 手动加锁
try {
// 临界区代码
} finally {
lock.unlock(); // 必须手动释放!
}
}

特点:



  • 🆕 JDK 1.5新秀,年轻有活力
  • 🎨 功能丰富,花样多
  • 🏗️ API级别实现,灵活强大
  • ⚠️ 需要手动管理,容易忘记释放

二、底层实现对决 💻


Round 1: synchronized的底层实现


1️⃣ 对象头结构(Mark Word)


Java对象内存布局:
┌────────────────────────────────────┐
│ 对象头 (Object Header) │
│ ┌─────────────────────────────┐ │
│ │ Mark Word (8字节) │ ← 存储锁信息
│ ├─────────────────────────────┤ │
│ │ 类型指针 (4/8字节) │ │
│ └─────────────────────────────┘ │
├────────────────────────────────────┤
│ 实例数据 (Instance Data) │
├────────────────────────────────────┤
│ 对齐填充 (Padding) │
└────────────────────────────────────┘

Mark Word在不同锁状态下的变化:


64位虚拟机的Mark Word(8字节=64位)

┌──────────────────────────────────────────────────┐
│ 无锁状态 (001) │
│ ┌────────────┬─────┬──┬──┬──┐ │
│ │ hashcode │ age │001│ 未锁定 │
│ └────────────┴─────┴──┴──┴──┘ │
├──────────────────────────────────────────────────┤
│ 偏向锁 (101) │
│ ┌────────────┬─────┬──┬──┬──┐ │
│ │ 线程ID │epoch│101│ 偏向锁 │
│ └────────────┴─────┴──┴──┴──┘ │
├──────────────────────────────────────────────────┤
│ 轻量级锁 (00) │
│ ┌────────────────────────────┬──┐ │
│ │ 栈中锁记录指针 │00│ 轻量级锁 │
│ └────────────────────────────┴──┘ │
├──────────────────────────────────────────────────┤
│ 重量级锁 (10) │
│ ┌────────────────────────────┬──┐ │
│ │ Monitor对象指针 │10│ 重量级锁 │
│ └────────────────────────────┴──┘ │
└──────────────────────────────────────────────────┘

2️⃣ 锁升级过程(重点!)


                    锁升级路径

无锁状态 偏向锁 轻量级锁 重量级锁
│ │ │ │
│ 第一次访问 │ 有竞争 │ 竞争激烈 │
├──────────────→ ├──────────────→ ├──────────────→ │
│ │ │ │
│ │ CAS失败 │ 自旋失败 │
│ │ │ │

🚶 一个人 🚶 还是一个人 🚶🚶 两个人 🚶🚶🚶 一群人
走路 (偏向这个人) 抢着走 排队走

详细解释:


阶段1:无锁 → 偏向锁


// 第一次有线程访问synchronized块
Thread-1第一次进入:
1. 对象处于无锁状态
2. Thread-1通过CAS在Mark Word中记录自己的线程ID
3. 成功!升级为偏向锁,偏向Thread-1
4. 下次Thread-1再来,发现Mark Word里是自己的ID,直接进入!
(就像VIP通道,不用检查)✨

生活比喻:
你第一次去常去的咖啡店☕,店员记住了你的脸。
下次你来,店员一看是你,直接给你做你的老口味,不用问!

阶段2:偏向锁 → 轻量级锁


Thread-2也想进入:
1. 发现偏向锁偏向的是Thread-1
2. Thread-1已经退出了,撤销偏向锁
3. 升级为轻量级锁
4. Thread-2通过CAS在栈帧中创建Lock Record
5. CAS将对象头的Mark Word复制到Lock Record
6. CAS将对象头指向Lock Record
7. 成功!获取轻量级锁 🎉

生活比喻:
咖啡店来了第二个客人,店员发现需要排队系统了。
拿出号码牌,谁先抢到谁先点单(自旋CAS)🎫

阶段3:轻量级锁 → 重量级锁


Thread-3、Thread-4、Thread-5也来了:
1. 多个线程竞争,CAS自旋失败
2. 自旋一定次数后,升级为重量级锁
3. 没抢到的线程进入阻塞队列
4. 需要操作系统介入,线程挂起(park)😴

生活比喻:
咖啡店人太多了!需要叫号系统 + 座位等待区。
没叫到号的人坐下来等,不用一直站着抢(操作系统介入)🪑

3️⃣ 字节码层面


public synchronized void method() {
System.out.println("hello");
}

字节码:


public synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED 看这里!方法标记
Code:
stack=2, locals=1, args_size=1
0: getstatic #2
3: ldc #3
5: invokevirtual #4
8: return

同步块字节码:


public void method() {
synchronized(this) {
System.out.println("hello");
}
}

public void method();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter 进入monitor
4: getstatic #2
7: ldc #3
9: invokevirtual #4
12: aload_1
13: monitorexit 退出monitor
14: goto 22
17: astore_2
18: aload_1
19: monitorexit 异常时也要退出
20: aload_2
21: athrow
22: return

Round 2: ReentrantLock的底层实现


基于AQS(AbstractQueuedSynchronizer)实现:


// ReentrantLock内部
public class ReentrantLock {
private final Sync sync;

// 抽象同步器
abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
}

// 非公平锁实现
static final class NonfairSync extends Sync {
final void lock() {
// 先CAS抢一次
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 进入AQS队列
}
}

// 公平锁实现
static final class FairSync extends Sync {
final void lock() {
acquire(1); // 直接排队,不插队
}
}
}

数据结构:


ReentrantLock

├─ Sync (继承AQS)
│ ├─ state: int (0=未锁,>0=重入次数)
│ └─ exclusiveOwnerThread: Thread (持锁线程)

└─ CLH队列
Head → Node1 → Node2 → Tail
↓ ↓
Thread2 Thread3
(等待) (等待)

三、功能对比大战 ⚔️


🏁 功能对比表


功能synchronizedReentrantLock胜者
加锁方式自动手动lock/unlocksynchronized ✅
释放方式自动(异常也会释放)必须手动finallysynchronized ✅
公平锁不支持支持公平/非公平ReentrantLock ✅
可中断不可中断lockInterruptibly()ReentrantLock ✅
尝试加锁不支持tryLock()ReentrantLock ✅
超时加锁不支持tryLock(timeout)ReentrantLock ✅
Condition只有一个wait/notify可多个ConditionReentrantLock ✅
性能(JDK6+)优化后差不多差不多平局 ⚖️
使用难度简单复杂,易出错synchronized ✅
锁信息不易查看getQueueLength()等ReentrantLock ✅

🎯 详细功能对比


1️⃣ 可中断锁


// ❌ synchronized不可中断
Thread t = new Thread(() -> {
synchronized(lock) {
// 即使调用t.interrupt(),这里也不会响应
while(true) {
// 死循环
}
}
});

// ✅ ReentrantLock可中断
Thread t = new Thread(() -> {
try {
lock.lockInterruptibly(); // 可响应中断
// ...
} catch (InterruptedException e) {
System.out.println("被中断了!");
}
});
t.start();
Thread.sleep(100);
t.interrupt(); // 可以中断!

2️⃣ 尝试加锁


// ❌ synchronized没有tryLock
synchronized(lock) {
// 要么拿到锁,要么一直等
}

// ✅ ReentrantLock可以尝试
if (lock.tryLock()) { // 尝试获取,不阻塞
try {
// 拿到锁了
} finally {
lock.unlock();
}
} else {
// 没拿到,去做别的事
System.out.println("锁被占用,我去干别的");
}

// ✅ 还支持超时
if (lock.tryLock(3, TimeUnit.SECONDS)) { // 等3秒
try {
// 拿到了
} finally {
lock.unlock();
}
} else {
// 3秒还没拿到,放弃
System.out.println("等太久了,不等了");
}

3️⃣ 公平锁


// ❌ synchronized只能是非公平锁
synchronized(lock) {
// 后来的线程可能插队
}

// ✅ ReentrantLock可选公平/非公平
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁(默认)

公平锁 vs 非公平锁:


非公平锁(吞吐量高):
Thread-1持锁 → Thread-2排队 → Thread-3排队

释放锁!

Thread-4刚好来了,直接抢!(插队)✂️
虽然Thread-2先来,但Thread-4先抢到

公平锁(先来后到):
Thread-1持锁 → Thread-2排队 → Thread-3排队

释放锁!

Thread-4来了,但要排队到最后!
Thread-2先到先得 ✅

4️⃣ 多个条件变量


// ❌ synchronized只有一个等待队列
synchronized(lock) {
lock.wait(); // 只有一个等待队列
lock.notify(); // 随机唤醒一个
}

// ✅ ReentrantLock可以有多个Condition
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 条件1:未满
Condition notEmpty = lock.newCondition(); // 条件2:非空

// 生产者
lock.lock();
try {
while (queue.isFull()) {
notFull.await(); // 等待"未满"条件
}
queue.add(item);
notEmpty.signal(); // 唤醒"非空"条件的线程
} finally {
lock.unlock();
}

// 消费者
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 等待"非空"条件
}
queue.remove();
notFull.signal(); // 唤醒"未满"条件的线程
} finally {
lock.unlock();
}

四、性能对决 🏎️


JDK 1.5时代:ReentrantLock完胜


JDK 1.5性能测试(100万次加锁):
synchronized: 2850ms 😓
ReentrantLock: 1200ms 🚀

ReentrantLock快2倍多!

JDK 1.6之后:synchronized反击!


JDK 1.6对synchronized做了大量优化:



  • ✅ 偏向锁(Biased Locking)
  • ✅ 轻量级锁(Lightweight Locking)
  • ✅ 自适应自旋(Adaptive Spinning)
  • ✅ 锁粗化(Lock Coarsening)
  • ✅ 锁消除(Lock Elimination)

JDK 1.8性能测试(100万次加锁):
synchronized: 1250ms 🚀
ReentrantLock: 1200ms 🚀

几乎一样了!

优化技术解析


1️⃣ 偏向锁


// 同一个线程反复进入
for (int i = 0; i < 1000000; i++) {
synchronized(obj) {
// 偏向锁:第一次CAS,后续直接进入
// 性能接近无锁!✨
}
}

2️⃣ 锁消除


public String concat(String s1, String s2) {
// StringBuffer是线程安全的,有synchronized
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}

// JVM发现sb是局部变量,不可能有竞争
// 自动消除StringBuffer内部的synchronized!
// 性能大幅提升!🚀

3️⃣ 锁粗化


// ❌ 原代码:频繁加锁解锁
for (int i = 0; i < 1000; i++) {
synchronized(obj) {
// 很短的操作
}
}

// ✅ JVM优化后:锁粗化
synchronized(obj) { // 把锁提到循环外
for (int i = 0; i < 1000; i++) {
// 很短的操作
}
}

五、使用场景推荐 📝


优先使用synchronized的场景


1️⃣ 简单的同步场景


// 简单的计数器
private int count = 0;

public synchronized void increment() {
count++;
}

2️⃣ 方法级别的同步


public synchronized void method() {
// 整个方法同步,简单明了
}

3️⃣ 不需要高级功能


// 只需要基本的互斥,不需要tryLock、Condition等
synchronized(lock) {
// 业务代码
}

优先使用ReentrantLock的场景


1️⃣ 需要可中断的锁


// 可以响应中断,避免死锁
lock.lockInterruptibly();

2️⃣ 需要尝试加锁


// 拿不到锁就去做别的事
if (lock.tryLock()) {
// ...
}

3️⃣ 需要公平锁


// 严格按照先来后到
ReentrantLock fairLock = new ReentrantLock(true);

4️⃣ 需要多个条件变量


// 生产者-消费者模式
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

5️⃣ 需要获取锁的信息


// 查看等待的线程数
int waiting = lock.getQueueLength();
// 查看是否有线程在等待
boolean hasWaiters = lock.hasQueuedThreads();

六、常见坑点 ⚠️


坑1:ReentrantLock忘记unlock


// ❌ 危险!如果中间抛异常,永远不会释放锁
lock.lock();
doSomething(); // 可能抛异常
lock.unlock(); // 不会执行!💣

// ✅ 正确写法
lock.lock();
try {
doSomething();
} finally {
lock.unlock(); // 一定会执行
}

坑2:synchronized锁错对象


// ❌ 每次都是新对象,不起作用!
public void method() {
synchronized(new Object()) { // 💣 错误!
// 相当于没加锁
}
}

// ✅ 正确写法
private final Object lock = new Object();
public void method() {
synchronized(lock) {
// ...
}
}

坑3:锁的粒度太大


// ❌ 锁的范围太大,性能差
public synchronized void method() { // 整个方法都锁住
doA(); // 不需要同步
doB(); // 需要同步
doC(); // 不需要同步
}

// ✅ 缩小锁范围
public void method() {
doA();
synchronized(lock) {
doB(); // 只锁需要的部分
}
doC();
}

七、面试应答模板 🎤


面试官:synchronized和ReentrantLock有什么区别?


你的回答:


主要从实现层面和功能层面两个角度对比:


实现层面:



  1. synchronized是JVM层面的,基于monitor机制,通过对象头的Mark Word实现
  2. ReentrantLock是API层面的,基于AQS(AbstractQueuedSynchronizer)实现

功能层面,ReentrantLock更强大:



  1. 可中断:lockInterruptibly()可响应中断
  2. 可尝试:tryLock()非阻塞获取锁
  3. 可超时:tryLock(time)超时放弃
  4. 公平锁:可选择公平或非公平
  5. 多条件:支持多个Condition
  6. 可监控:可获取等待线程数等信息

性能对比:



  • JDK 1.6之前ReentrantLock性能更好
  • JDK 1.6之后synchronized做了大量优化(偏向锁、轻量级锁、锁消除、锁粗化),性能差不多
  • synchronized优化包括:无锁→偏向锁→轻量级锁→重量级锁的升级路径

使用建议:



  • 简单场景优先synchronized(代码简洁,自动释放)
  • 需要高级功能时用ReentrantLock(可中断、超时、公平锁等)

举个例子:
如果只是简单的计数器,用synchronized即可。但如果是银行转账系统,需要可中断、可超时,就应该用ReentrantLock。


八、总结 🎯


选择决策树:
需要同步?

Yes

┌─────────────┴─────────────┐
│ │
简单场景 复杂场景
(计数器、缓存等) (可中断、超时等)
│ │
synchronized ReentrantLock
│ │
✅ 简单 ✅ 功能强
✅ 自动释放 ⚠️ 需手动
✅ 性能好 ✅ 灵活

记忆口诀:



简单场景synchronized,

复杂需求ReentrantLock,

性能现在差不多,

根据场景来选择!🎵



最后一句话:

synchronized是"自动挡"🚗,简单好用;

ReentrantLock是"手动挡"🏎️,灵活强大!


作者:用户6854537597769
来源:juejin.cn/post/7563822304766427172

0 个评论

要回复文章请先登录注册