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

一、选手登场!🎬

🔵 蓝方:synchronized(老牌选手)

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

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

特点:

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

🔴 红方:ReentrantLock(新锐选手)

java 复制代码
// 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)

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

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

scss 复制代码
64位虚拟机的Mark Word(8字节=64位)

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

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

markdown 复制代码
                    锁升级路径
                    
无锁状态              偏向锁              轻量级锁            重量级锁
  │                   │                   │                   │
  │    第一次访问      │    有竞争         │    竞争激烈       │
  ├──────────────→   ├──────────────→   ├──────────────→   │
  │                   │                   │                   │
  │                   │    CAS失败        │    自旋失败       │
  │                   │                   │                   │
  
  🚶 一个人         🚶 还是一个人       🚶🚶 两个人        🚶🚶🚶 一群人
   走路             (偏向这个人)        抢着走             排队走

详细解释:

阶段1:无锁 → 偏向锁

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

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

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

java 复制代码
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:轻量级锁 → 重量级锁

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

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

3️⃣ 字节码层面

java 复制代码
public synchronized void method() {
    System.out.println("hello");
}

字节码:

yaml 复制代码
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

同步块字节码:

java 复制代码
public void method() {
    synchronized(this) {
        System.out.println("hello");
    }
}
yaml 复制代码
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)实现:

java 复制代码
// 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);  // 直接排队,不插队
        }
    }
}

数据结构:

ini 复制代码
ReentrantLock
    │
    ├─ Sync (继承AQS)
    │    ├─ state: int (0=未锁,>0=重入次数)
    │    └─ exclusiveOwnerThread: Thread (持锁线程)
    │
    └─ CLH队列
         Head → Node1 → Node2 → Tail
                 ↓       ↓
              Thread2  Thread3
              (等待)   (等待)

三、功能对比大战 ⚔️

🏁 功能对比表

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

🎯 详细功能对比

1️⃣ 可中断锁

java 复制代码
// ❌ 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️⃣ 尝试加锁

java 复制代码
// ❌ 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️⃣ 公平锁

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

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

公平锁 vs 非公平锁:

markdown 复制代码
非公平锁(吞吐量高):
Thread-1持锁 → Thread-2排队 → Thread-3排队
         ↓
    释放锁!
         ↓
Thread-4刚好来了,直接抢!(插队)✂️
虽然Thread-2先来,但Thread-4先抢到

公平锁(先来后到):
Thread-1持锁 → Thread-2排队 → Thread-3排队
         ↓
    释放锁!
         ↓
Thread-4来了,但要排队到最后!
Thread-2先到先得 ✅

4️⃣ 多个条件变量

java 复制代码
// ❌ 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完胜

makefile 复制代码
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)
makefile 复制代码
JDK 1.8性能测试(100万次加锁):
synchronized:   1250ms  🚀
ReentrantLock:  1200ms  🚀

几乎一样了!

优化技术解析

1️⃣ 偏向锁

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

2️⃣ 锁消除

java 复制代码
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️⃣ 锁粗化

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

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

五、使用场景推荐 📝

优先使用synchronized的场景

1️⃣ 简单的同步场景

java 复制代码
// 简单的计数器
private int count = 0;

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

2️⃣ 方法级别的同步

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

3️⃣ 不需要高级功能

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

优先使用ReentrantLock的场景

1️⃣ 需要可中断的锁

java 复制代码
// 可以响应中断,避免死锁
lock.lockInterruptibly();

2️⃣ 需要尝试加锁

java 复制代码
// 拿不到锁就去做别的事
if (lock.tryLock()) {
    // ...
}

3️⃣ 需要公平锁

java 复制代码
// 严格按照先来后到
ReentrantLock fairLock = new ReentrantLock(true);

4️⃣ 需要多个条件变量

java 复制代码
// 生产者-消费者模式
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

5️⃣ 需要获取锁的信息

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

六、常见坑点 ⚠️

坑1:ReentrantLock忘记unlock

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

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

坑2:synchronized锁错对象

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

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

坑3:锁的粒度太大

java 复制代码
// ❌ 锁的范围太大,性能差
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。

八、总结 🎯

scss 复制代码
选择决策树:
                   需要同步?
                      │
                     Yes
                      │
        ┌─────────────┴─────────────┐
        │                           │
    简单场景                    复杂场景
 (计数器、缓存等)          (可中断、超时等)
        │                           │
   synchronized                ReentrantLock
        │                           │
    ✅ 简单                      ✅ 功能强
    ✅ 自动释放                  ⚠️ 需手动
    ✅ 性能好                    ✅ 灵活

记忆口诀:

简单场景synchronized,

复杂需求ReentrantLock,

性能现在差不多,

根据场景来选择!🎵

最后一句话:

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

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

相关推荐
fliter2 分钟前
在 Async Rust 中实现请求合并(Request Coalescing)
后端
王立志_LEO2 分钟前
Gunicorn 启动django服务
后端
fliter3 分钟前
一个让我调试一周的 Rust match 陷阱
后端
一只大袋鼠14 分钟前
SpringBoot 初学阶段知识点汇总(一)
spring boot·笔记·后端
Rust研习社17 分钟前
Rust 官方拟定 LLM 政策,防止 LLM 污染开源社区?
开发语言·后端·ai·rust·开源
无风听海33 分钟前
ASP.NET Core Minimal API 深度解析
后端·asp.net
IT_陈寒43 分钟前
Java的finally块竟然不是你想的那个finally!
前端·人工智能·后端
zb200641201 小时前
Laravel4.x核心特性全解析
spring boot·后端·php·laravel
techdashen1 小时前
在 Async Rust 中实现请求合并(Request Coalescing)
开发语言·后端·rust
lzp07911 小时前
C#如何优雅处理引用类型的深拷贝(贰)
spring boot·后端·ui