Java并发编程:Synchronized与ReentrantLock深度解析
1. Synchronized 与 ReentrantLock 深度对比在Java并发编程领域锁机制是保证线程安全的核心手段。Synchronized和ReentrantLock作为两种最常用的锁实现经常被拿来比较。但很多开发者对它们的理解仅停留在一个是关键字一个是类的层面这在实际高并发场景中远远不够。本文将基于JDK源码和实际压测数据从实现原理、性能表现到适用场景进行全面对比。2. 底层实现机制解析2.1 Synchronized的JVM实现Synchronized在JVM层面通过monitor实现同步编译后会在同步代码块前后插入monitorenter和monitorexit指令。在JDK1.6之前它直接对应操作系统的互斥锁Mutex Lock属于重量级锁。经过多次优化后现在采用锁升级策略偏向锁Biased LockingMark Word记录线程ID适用于无竞争场景轻量级锁Lightweight Locking通过CAS自旋尝试获取锁重量级锁Heavyweight Locking最终退化为操作系统互斥锁// 典型使用方式 public synchronized void method() { // 同步代码 } // 或 public void method() { synchronized(this) { // 同步代码 } }2.2 ReentrantLock的AQS实现ReentrantLock基于AbstractQueuedSynchronizerAQS框架实现其核心是通过CLH队列管理线程排队。与Synchronized不同它完全用Java代码实现主要特点包括显式获取和释放锁必须手动unlock支持公平锁和非公平锁两种模式提供Condition实现精确唤醒可中断的锁获取机制ReentrantLock lock new ReentrantLock(); public void method() { lock.lock(); // 必须在try外获取锁 try { // 同步代码 } finally { lock.unlock(); } }3. 关键特性对比3.1 锁的公平性特性SynchronizedReentrantLock公平锁仅非公平可配置构造参数决定非公平锁默认实现默认实现公平锁会严格按线程等待顺序分配锁但会带来显著的性能下降。实测在16核机器上公平模式吞吐量可能下降10倍以上。3.2 锁的可重入性两者都是可重入锁同一个线程可重复获取但实现方式不同Synchronized通过计数器记录重入次数ReentrantLock通过AQS的state字段记录3.3 锁的等待机制ReentrantLock提供更灵活的等待方式tryLock()尝试获取锁立即返回结果tryLock(long timeout, TimeUnit unit)超时等待lockInterruptibly()可响应中断的锁获取而Synchronized的锁获取是不可中断的可能造成死锁难以解除。4. 性能实测对比使用JMH进行基准测试单位ops/ms场景SynchronizedReentrantLock单线程无竞争152314864线程低竞争87691216线程高竞争23438732线程超高竞争89215在高竞争环境下ReentrantLock展现出明显优势主要因为更精细的锁控制策略避免了重量级锁的上下文切换优化的CAS自旋机制5. 内存语义与可见性两者都遵循happens-before原则保证可见性但实现机制不同Synchronized解锁前会将工作内存刷新到主内存ReentrantLock依赖volatile变量state和CAS操作在极端情况下ReentrantLock的内存开销略高因为需要维护等待队列。6. 典型使用场景建议6.1 优先使用Synchronized的情况简单的同步块如单方法同步低竞争或已知竞争程度不高的场景需要最小化依赖不引入java.util.concurrent包代码可读性要求高的场景6.2 优先使用ReentrantLock的情况需要尝试获取锁tryLock需要超时或可中断的锁获取需要公平锁策略需要绑定多个Condition高竞争环境下对性能有严格要求7. 常见问题与避坑指南7.1 锁泄露问题ReentrantLock必须手动释放典型错误模式lock.lock(); try { if(condition) { return; // 提前返回导致锁未释放 } } finally { lock.unlock(); // 应该放在更外层的finally块 }最佳实践确保unlock()在方法最外层的finally块中调用7.2 死锁排查两者都可能引发死锁但表现不同Synchronized死锁jstack会显示BLOCKED状态和持有锁信息ReentrantLock死锁线程可能处于WAITING状态等待Condition使用jcmd Thread.print可以获取更详细的锁信息。7.3 性能调优技巧对于Synchronized减小同步块范围避免在同步块内调用耗时操作考虑使用偏向锁-XX:UseBiasedLocking对于ReentrantLock合理设置自旋次数非公平锁通常性能更好避免频繁创建新实例重用锁对象8. 源码级实现差异8.1 Synchronized的锁升级路径HotSpot虚拟机的对象头Mark Word变化无锁 → 偏向锁 → 轻量级锁 → 重量级锁转换条件由JVM的启发式算法决定可通过-XX:BiasedLockingStartupDelay0参数立即启用偏向锁。8.2 AQS的队列管理ReentrantLock的等待队列实现关键// AbstractQueuedSynchronizer部分源码 private transient volatile Node head; private transient volatile Node tail; private volatile int state; static final class Node { volatile Thread thread; volatile Node next; volatile int waitStatus; }通过CAS操作维护队列避免使用真正的锁结构。9. 扩展应用场景9.1 读写锁的实现基于ReentrantLock可以构建更复杂的锁策略ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); Lock readLock rwLock.readLock(); // 共享锁 Lock writeLock rwLock.writeLock(); // 排他锁9.2 分布式锁的本地模拟通过扩展ReentrantLock可以实现简单的分布式锁模拟class DistributedLockAdapter extends ReentrantLock { // 添加网络通信逻辑 public boolean tryLock(long timeout, TimeUnit unit) { // 先尝试本地锁 // 成功后再尝试获取分布式锁 } }10. 最新JDK中的改进JDK15对Synchronized的优化偏向锁延迟降低自旋策略更智能与虚拟线程Loom项目更好协作ReentrantLock在JDK17中的变化更高效的CAS操作内存屏障优化与ScopedValue更好集成在实际编码中我发现对于95%的常规同步需求Synchronized已经完全够用。但在需要精细控制的高并发场景ReentrantLock仍然是不可或缺的工具。一个经验法则是当你在考虑是否该用ReentrantLock时可能真的需要它了如果从未想过这个问题Synchronized通常就是最佳选择。