⚔️ 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是"手动挡"🏎️,灵活强大!

相关推荐
Cache技术分享4 小时前
217. Java 函数式编程风格 - 从命令式到函数式:基于条件选择元素
前端·后端
用户68545375977694 小时前
CopyOnWriteArrayList:写时复制的艺术🎨
后端
用户68545375977694 小时前
线程安全过期缓存:手写Guava Cache🗄️
后端
用户68545375977694 小时前
🔄 ConcurrentHashMap进化史:从分段锁到CAS+synchronized
后端
程序员小凯4 小时前
Spring Boot API文档与自动化测试详解
java·spring boot·后端
数据小馒头4 小时前
Web原生架构 vs 传统C/S架构:在数据库管理中的性能与安全差异
后端
用户68545375977694 小时前
🔑 AQS抽象队列同步器:Java并发编程的"万能钥匙"
后端
yren4 小时前
Mysql 多版本并发控制 MVCC
后端
回家路上绕了弯4 小时前
外卖员重复抢单?从技术到运营的全链路解决方案
分布式·后端