ReentrantLock 与 synchronized对比

0 阅读6分钟

以下是 ReentrantLocksynchronized 的全面对比解析,涵盖实现机制、特性差异、性能演进、选型指南及代码示例。


ReentrantLock vs synchronized 完全对比手册


一、核心实现机制对比

维度synchronizedReentrantLock
实现层级JVM 内置(C++ 实现,依赖对象头 Mark Word 和 Monitor)JDK 层 API(Java 实现,基于 AQS 框架)
锁状态存储对象头中的 Mark Word(记录锁状态、持有线程、重入次数等)AQS 的 volatile int state 字段
等待队列每个对象关联一个 Monitor,内部维护 _cxq_EntryList_WaitSet 三个队列AQS 内部维护一个 FIFO 的 CLH 变种队列
条件变量单个隐式条件队列(wait/notify/notifyAll支持多个 Condition 对象,每个维护独立条件队列
锁升级机制偏向锁 → 轻量级锁 → 重量级锁(JDK 6+ 优化)无锁升级,直接基于 CAS 和队列实现重量级互斥
释放方式自动释放(代码块结束或异常)必须显式释放unlock() 通常在 finally 中)

二、特性功能全面对比

功能特性synchronizedReentrantLock说明
可重入性✅ 支持✅ 支持同一线程可重复获取已持有的锁
公平锁❌ 仅非公平✅ 构造函数指定 true 为公平锁公平锁保证 FIFO 顺序,但吞吐量低
可中断获取❌ 阻塞时不可中断lockInterruptibly()等待锁时可响应 Thread.interrupt()
超时获取❌ 不支持tryLock(timeout, unit)允许在指定时间内尝试获取锁
非阻塞尝试❌ 不支持tryLock()立即返回,不排队等待
多条件等待❌ 仅单个隐式条件✅ 多个 Condition可实现精确唤醒,避免惊群效应
状态监控❌ 无直接 APIgetQueueLength()hasQueuedThreads()便于运维监控和性能调优
锁降级/升级❌ 不支持❌ 不支持两者均不直接支持锁降级

三、底层源码实现对比

1. synchronized 底层(JVM 层面)
// 简化示意:synchronized 代码块编译后对应 monitorenter / monitorexit 指令
// 实际 JVM 实现涉及 ObjectMonitor 结构

class ObjectMonitor {
  _header       = NULL;          // 对象头备份
  _count        = 0;             // 重入次数
  _waiters      = 0;             // 等待线程数
  _recursions   = 0;             // 递归次数(同 count)
  _owner        = NULL;          // 持有线程
  _WaitSet      = NULL;          // wait() 线程队列
  _EntryList    = NULL;          // 阻塞等待锁的线程队列
  ...
};

// 进入同步块:monitorenter
// 1. 如果当前对象无锁(偏向/轻量级未膨胀),尝试 CAS 获取偏向锁/轻量级锁
// 2. 失败则膨胀为重量级锁,进入 _EntryList 等待
// 3. 重入时直接递增 _recursions

// 退出同步块:monitorexit
// 1. 递减 _recursions,若归零则释放锁
// 2. 若 _EntryList 不为空,唤醒队首线程
2. ReentrantLock 底层(AQS 层面)
// 核心:基于 AQS 的 state 变量和 CLH 队列

public class ReentrantLock implements Lock {
    private final Sync sync;
    
    abstract static class Sync extends AbstractQueuedSynchronizer {
        // 重入计数 state
        // state == 0: 锁空闲
        // state > 0: 锁被持有,值为重入次数
        
        // 释放锁钩子
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
    }
    
    // 非公平锁获取逻辑
    static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))   // 第一次插队
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);                 // 进入 AQS 排队
        }
        
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires); // 第二次插队机会
        }
    }
}

四、性能演进与现状

JDK 版本synchronized 性能状态ReentrantLock 性能状态建议
JDK 1.5 之前纯重量级锁,每次加锁都涉及系统调用,性能极差尚未引入(java.util.concurrent 包在 1.5 发布)无选择余地
JDK 1.5重量级锁刚推出,基于 AQS,性能远优于 synchronized优先使用 ReentrantLock
JDK 1.6引入偏向锁、轻量级锁、自旋锁优化,性能大幅提升稳定,性能优秀两者性能接近,按功能需求选择
JDK 1.8+持续优化(如锁消除、锁粗化),低竞争下几乎无开销性能稳定低竞争无差别,高竞争 ReentrantLock 略优(但差距很小)

当前结论:在大多数常见场景下,两者性能处于同一量级,不应再将性能作为首选区分因素。应基于功能需求代码安全性做出选择。


五、代码风格与安全性对比

对比点synchronizedReentrantLock
代码简洁性⭐⭐⭐⭐⭐ 无需手动释放,代码块即锁范围⭐⭐ 必须显式 lock()/unlock()
异常安全性⭐⭐⭐⭐⭐ 自动释放,无死锁风险⭐⭐⭐ 若忘记 finally 释放,死锁
灵活控制⭐⭐ 无法中断、超时、非阻塞⭐⭐⭐⭐⭐ 提供多种锁获取方式
条件同步⭐⭐ 只能 wait/notify 在单个对象上⭐⭐⭐⭐⭐ 多 Condition 精准控制
可调试性⭐⭐⭐ 难以查询锁状态⭐⭐⭐⭐ 提供 getQueueLength() 等监控 API

示例对比

// synchronized 风格 —— 简单安全
synchronized (lock) {
    doSomething();
}

// ReentrantLock 风格 —— 灵活但需谨慎
lock.lock();
try {
    doSomething();
} finally {
    lock.unlock();
}

六、典型场景选型指南

优先使用 synchronized 的场景
  1. 简单互斥:仅需保护临界区,无特殊功能要求。
  2. 方法级同步:直接在方法上使用 synchronized 关键字。
  3. 团队规范:团队对 ReentrantLock 不熟悉,易用错导致死锁。
  4. 性能非瓶颈:临界区执行时间极短,锁竞争不激烈。
必须使用 ReentrantLock 的场景
场景所需特性示例
需要超时放弃tryLock(timeout)获取数据库连接,等待 500ms 无果则降级返回缓存
需要响应中断lockInterruptibly()用户点击取消,立即终止等待锁的线程
需要公平锁new ReentrantLock(true)任务调度系统严格按提交顺序执行
需要多条件精准唤醒多个 Condition有界阻塞队列,生产者/消费者分别唤醒对方
需要监控锁状态getQueueLength()运维监控系统,告警等待线程数过多
复杂锁交互非阻塞尝试 tryLock()避免死锁,尝试获取多个锁

七、混合使用注意事项

  1. 不要混用:同一个共享资源的同步不要混用两种锁机制,极易出错。
  2. synchronized 不可与 Condition 配合Condition 必须与 Lock 绑定。
  3. wait/notify 必须在 synchronized 块内,而 await/signal 必须在 Lock 块内。

八、总结对比表(一图看懂)

┌─────────────────────┬────────────────────────────┬────────────────────────────┐
│      维度           │      synchronized          │       ReentrantLock        │
├─────────────────────┼────────────────────────────┼────────────────────────────┤
│ 实现层              │ JVM (C++)                  │ JDK (Java)                 │
│ 释放方式            │ 自动                       │ 手动(必须 finally)        │
│ 公平锁              │ ❌                         │ ✅                         │
│ 可中断获取          │ ❌                         │ ✅                         │
│ 超时获取            │ ❌                         │ ✅                         │
│ 非阻塞尝试          │ ❌                         │ ✅                         │
│ 多条件变量          │ ❌ (单个 wait/notify)       │ ✅ (多个 Condition)         │
│ 状态监控            │ ❌                         │ ✅                         │
│ 性能(低竞争)      │ 相当                       │ 相当                        │
│ 性能(高竞争)      │ 相当(JVM 持续优化)        │ 略优(但差距已极小)         │
│ 代码复杂度          │ 低                         │ 中~高                      │
│ 适用场景            │ 大多数普通同步需求           │ 需要高级控制功能的复杂场景    │
└─────────────────────┴────────────────────────────┴────────────────────────────┘

九、最终建议

默认首选 synchronized —— 简洁、安全、不易出错,满足 90% 的同步需求。

当且仅当需要以下特性时,才升级到 ReentrantLock

  • 需要超时、可中断、非阻塞的锁获取
  • 需要公平锁保证顺序
  • 需要多个条件变量实现精准线程调度
  • 需要监控锁的运行时状态

记住:简单的代码往往更可靠。不要为了“炫技”而滥用 ReentrantLock