有一个朋友跳槽到了新公司,接手的首个任务就是排查线上死锁问题。两个接口相互调用,偶尔会出现卡死现象,且只在流量高峰期爆发。他用jstack查看后发现,两个线程都在等待对方释放锁,这是典型的死锁场景。
但真正引发思考的是,他提到一个细节:其中一个接口使用了synchronized,另一个则用了ReentrantLock。这不禁让人追问——既然两种锁都能实现互斥,为什么Java要设计两套锁机制?它们的底层实现究竟有何差异?在什么场景下应该选择哪一个?
深入源码分析后发现,这个问题远比表面复杂。synchronized从JVM的对象头Mark Word到锁升级机制,ReentrantLock从AQS的state变量到CLH队列,每层设计都蕴含精妙之处。更关键的是,理解这些原理才能真正掌握:为什么有些场景用synchronized性能更优,而另一些场景则必须依赖ReentrantLock。
这篇文章将带你深入Java锁机制的核心原理,通过对比分析、源码解析和实战案例,帮助你构建完整的知识体系。
二、synchronized深度解析
2.1 synchronized的实现原理
synchronized是Java语言内置的锁机制,称它为"亲儿子"并不过分。它的实现完全依赖JVM,核心在于对象头和Monitor机制。
对象头结构
每个Java对象在JVM中都有一个对象头,其中的Mark Word部分存储了锁状态信息:
图片

Monitor机制
当synchronized修饰方法或代码块时,JVM会通过Monitor来实现互斥:
public class SynchronizedDemo { // 方法级锁 public synchronized void method() { // 业务逻辑 } // 代码块锁 public void block() { synchronized(this) { // 业务逻辑 } }}
在字节码层面:
方法级锁:通过ACC_SYNCHRONIZED标志隐式实现
代码块锁:通过monitorenter和monitorexit指令显式实现
synchronized锁升级流程图:
图片
上图展示了synchronized从无锁到重量级锁的完整升级路径:
无锁状态:对象刚创建,没有任何线程访问
偏向锁:第一个线程访问时,在对象头记录线程ID
轻量级锁:多个线程交替访问时,使用CAS自旋
重量级锁:竞争激烈时,使用Monitor阻塞等待
2.2 锁升级详细过程
偏向锁
偏向锁的假设很有意思:它认为锁主要由同一个线程多次获得。于是通过在对象头中记录线程ID来避免同步,类似于给锁贴上了"专属标签":
// 偏向锁开启参数-XX:+UseBiasedLocking-XX:BiasedLockingStartupDelay=0
偏向锁获取流程:
检查Mark Word是否为可偏向状态(偏向锁标志=1,锁标志=01)
如果是,检查线程ID是否为当前线程
如果是,直接进入同步块
如果不是,通过CAS尝试将线程ID设置为当前线程ID
轻量级锁
当有其他线程来"抢"偏向锁时,就会升级为轻量级锁:
// 轻量级锁获取过程1. 在当前线程栈帧中创建Lock Record2. 将对象头的Mark Word复制到Lock Record3. 用CAS尝试将对象头指向Lock Record4. 如果成功,获得轻量级锁5. 如果失败,说明有竞争,膨胀为重量级锁
轻量级锁使用自旋锁,旨在避免线程切换开销:
默认自旋次数:10次
JDK 6后引入自适应自旋:根据历史自旋成功率动态调整
重量级锁
轻量级锁自旋失败后,会升级为重量级锁:
// ObjectMonitor核心结构class ObjectMonitor { ObjectMonitor() { _header = NULL; _count = 0; // 锁计数器 _waiters = 0, _recursions = 0; // 重入次数 _owner = NULL; // 持有锁的线程 _WaitSet = NULL; // 等待队列 _EntryList = NULL; // 阻塞队列 }}
重量级锁基于操作系统的Mutex Lock实现:
线程获取锁失败后会被阻塞
线程切换需要从用户态切换到内核态
性能开销较大,但能保证公平性
2.3 synchronized实战案例
案例1:单例模式双重检查
public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); } } } return instance; }}
为什么要双重检查?
第一个if:避免不必要的锁竞争
synchronized:保证线程安全
第二个if:防止重复创建
volatile:禁止指令重排序
案例2:线程安全的计数器
public class SynchronizedCounter { private int count = 0; public synchronized void increment() { count++; } public synchronized void decrement() { count--; } public synchronized int getCount() { return count; }}
synchronized保证了:
原子性:count++操作的原子性
可见性:修改后立即刷新到主内存
有序性:防止指令重排序
三、ReentrantLock核心原理
3.1 ReentrantLock基本使用
ReentrantLock是JDK提供的可重入锁,基于AQS(AbstractQueuedSynchronizer)实现,使用起来比synchronized更灵活,但相应地,也需要开发者自行管理锁的获取和释放:
public class ReentrantLockDemo { private final ReentrantLock lock = new ReentrantLock(); private int count = 0; public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } // 可中断锁 public void incrementWithInterrupt() throws InterruptedException { lock.lockInterruptibly(); try { count++; } finally { lock.unlock(); } } // 尝试获取锁 public boolean tryIncrement() { if (lock.tryLock()) { try { count++; return true; } finally { lock.unlock(); } } return false; }}
3.2 AQS核心原理
AQS是ReentrantLock的核心,它使用一个volatile int state字段和一个CLH队列实现:
public abstract class AbstractQueuedSynchronizer { // 同步状态 private volatile int state; // 等待队列头节点 private transient volatile Node head; // 等待队列尾节点 private transient volatile Node tail; // 队列节点 static final class Node { volatile Node prev; // 前驱节点 volatile Node next; // 后继节点 volatile Thread thread; // 等待线程 volatile int waitStatus; // 等待状态 }}
公平锁vs非公平锁
// 非公平锁(默认)public ReentrantLock() { sync = new NonfairSync();}// 公平锁public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();}
公平锁与非公平锁对比:
非公平锁:新线程直接尝试CAS,不检查队列
公平锁:新线程检查队列中是否有等待线程,有则排队
3.3 Condition实现原理
Condition提供了类似wait/notify的功能,但更加强大,因为它允许多个条件队列,实现更精细的线程协作:
public class ConditionDemo { private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); private final Object[] items = new Object[10]; private int putIndex, takeIndex, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) { notFull.await(); // 队列满,等待 } items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++; notEmpty.signal(); // 唤醒消费者 } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) { notEmpty.await(); // 队列空,等待 } Object x = items[takeIndex]; if (++takeIndex == items.length) takeIndex = 0; count--; notFull.signal(); // 唤醒生产者 return x; } finally { lock.unlock(); } }}
四、CAS机制详解
4.1 CAS基本概念
CAS(Compare And Swap)是乐观锁的核心实现,其思想是"先尝试,失败再重试",从而避免线程阻塞:
public class CASDemo { private AtomicInteger count = new AtomicInteger(0); public void increment() { int oldValue, newValue; do { oldValue = count.get(); newValue = oldValue + 1; } while (!count.compareAndSet(oldValue, newValue)); }}
CAS操作包含三个操作数:
V:内存值
E:预期值
N:新值
只有当V==E时,才会将V设置为N。
CAS操作原理图:
图片
上图展示了CAS的完整执行流程:
读取内存值:从主内存读取当前值
计算新值:根据业务逻辑计算新值
CAS操作:比较内存值与预期值
成功/失败:成功则更新,失败则重试
4.2 ABA问题与解决方案
ABA问题是CAS的经典问题,也是面试中常被问到的陷阱之一:
public class ABAProblem { private static AtomicInteger atomicInt = new AtomicInteger(100); public static void main(String[] args) { // 线程1 new Thread(() -> { atomicInt.compareAndSet(100, 101); atomicInt.compareAndSet(101, 100); }).start(); // 线程2 new Thread(() -> { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } boolean success = atomicInt.compareAndSet(100, 101); System.out.println("CAS成功: " + success); // true }).start(); }}
线程2无法感知值被修改过,这就是ABA问题。
解决方案:AtomicStampedReference
public class ABASolution { private static AtomicStampedReference
AtomicStampedReference通过引入版本号机制,每次修改都附带一个"印记",从根本上解决了ABA问题。
五、锁优化策略
JVM在锁优化方面下了不少功夫,其中有几个策略非常关键,理解它们对编写高性能并发代码很有帮助。
5.1 锁消除
JVM通过逃逸分析,能够识别出那些不会被外部访问的对象,从而消除它们上面的锁:
public class LockElimination { public String concat(String s1, String s2) { // StringBuffer是线程安全的,但在这个方法中 // sb不会逃逸,所以JVM会消除锁 StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }}
5.2 锁粗化
JVM会将相邻的锁操作合并,以减少频繁加锁和解锁的开销:
public class LockCoarsening { private final Object lock = new Object(); public void method() { // JVM会将这3个锁合并为1个锁 synchronized(lock) { // 操作1 } synchronized(lock) { // 操作2 } synchronized(lock) { // 操作3 } }}
5.3 锁分离
读写分离,将一把锁拆成读锁和写锁,读读不互斥,大幅提升并发性能:
public class ReadWriteLockDemo { private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); private final Lock readLock = rwLock.readLock(); private final Lock writeLock = rwLock.writeLock(); private Map
六、synchronized vs ReentrantLock对比
6.1 功能对比
6.2 性能对比
public class LockPerformanceTest { private static final int THREAD_COUNT = 16; private static final int OPERATIONS = 1000000; // synchronized测试 private static int synchronizedCount = 0; private static final Object lock = new Object(); public static void synchronizedIncrement() { synchronized(lock) { synchronizedCount++; } } // ReentrantLock测试 private static int reentrantLockCount = 0; private static final ReentrantLock reentrantLock = new ReentrantLock(); public static void reentrantLockIncrement() { reentrantLock.lock(); try { reentrantLockCount++; } finally { reentrantLock.unlock(); } }}
测试结果(operations/ms):
结论:JDK 6之后,synchronized经过了全方位优化,性能已经和ReentrantLock非常接近。因此,不必再纠结于性能差异,关键是看场景。
6.3 选择建议
使用synchronized的场景:
简单的同步需求
不需要高级特性(中断、超时等)
代码可读性优先
已经有成熟的JVM优化
使用ReentrantLock的场景:
需要公平锁
需要中断响应
需要超时获取锁
需要多个条件变量
需要精细的锁控制
七、实战踩坑指南
理论和源码看完了,但真正写代码时,一些细节上的坑很容易让人"翻车"。下面这几个问题,遇到时都非常令人头疼。
7.1 坑1:锁对象选择错误
// ❌ 错误示例public class BadLock { private Integer count = 0; public void increment() { synchronized(count) { // Integer缓存,可能锁同一个对象 count++; } }}// ✅ 正确示例public class GoodLock { private final Object lock = new Object(); private Integer count = 0; public void increment() { synchronized(lock) { count++; } }}
7.2 坑2:锁粒度过大
// ❌ 错误示例public class BadGranularity { private final Map
7.3 坑3:死锁问题
// ❌ 死锁示例public class DeadlockDemo { private final Object lock1 = new Object(); private final Object lock2 = new Object(); public void method1() { synchronized(lock1) { synchronized(lock2) { // 业务逻辑 } } } public void method2() { synchronized(lock2) { synchronized(lock1) { // 业务逻辑 } } }}// ✅ 避免死锁:统一锁顺序public class NoDeadlockDemo { private final Object lock1 = new Object(); private final Object lock2 = new Object(); public void method1() { synchronized(lock1) { synchronized(lock2) { // 业务逻辑 } } } public void method2() { synchronized(lock1) { // 统一先获取lock1 synchronized(lock2) { // 再获取lock2 // 业务逻辑 } } }}
7.4 坑4:忘记释放锁
// ❌ 错误示例public class BadRelease { private final ReentrantLock lock = new ReentrantLock(); public void method() { lock.lock(); // 业务逻辑,可能抛异常 lock.unlock(); // 异常时不会执行 }}// ✅ 正确示例public class GoodRelease { private final ReentrantLock lock = new ReentrantLock(); public void method() { lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); // 始终在finally中释放 } }}
八、总结
8.1 核心知识点
通过这篇文章,我们深入理解了Java锁机制的核心原理:
1. synchronized实现机制
对象头Mark Word存储锁状态
锁升级:偏向锁→轻量级锁→重量级锁
JVM层面的Monitor机制
2. ReentrantLock核心原理
基于AQS的CLH队列实现
支持公平锁和非公平锁
提供中断、超时、条件变量等高级特性
3. CAS机制
乐观锁的核心实现
ABA问题及解决方案
AtomicStampedReference版本号机制
4. 锁优化策略
锁消除:逃逸分析
锁粗化:合并相邻锁
锁分离:读写分离
8.2 最佳实践
优先使用synchronized:简单场景,JVM优化好
高级特性用ReentrantLock:公平锁、中断、超时
锁对象要稳定:使用final Object
锁粒度要适中:避免过大或过小
释放锁要在finally:保证锁一定释放
避免死锁:统一锁顺序,使用tryLock
九、面试加分项(Q&A)
Q1:synchronized和ReentrantLock有什么区别?
synchronized和ReentrantLock都能实现线程同步,但有本质区别。synchronized是JVM层面的锁,通过对象头和Monitor实现,支持锁升级优化,使用简单但功能有限。ReentrantLock是JDK API层面的锁,基于AQS实现,提供公平/非公平选择、中断响应、超时获取、多条件变量等高级特性,使用灵活但需要手动释放锁。
性能方面,JDK 6后synchronized经过优化(偏向锁、轻量级锁、自适应自旋),性能已接近ReentrantLock。选择建议:简单场景用synchronized,需要高级特性时用ReentrantLock。
Q2:synchronized的锁升级过程是怎样的?为什么要这样设计?
synchronized锁升级过程:无锁→偏向锁→轻量级锁→重量级锁。偏向锁假设锁主要由同一线程获得,在对象头记录线程ID避免同步;轻量级锁在多线程交替访问时使用CAS自旋,避免阻塞;重量级锁在竞争激烈时使用Monitor阻塞等待。
这样设计是为了平衡性能和公平性。偏向锁和轻量级锁在无竞争或弱竞争时性能高,避免操作系统级别的阻塞;重量级锁在强竞争时保证公平性,避免CPU空转。这种自适应策略让synchronized在大多数场景下性能优异。
Q3:什么是ABA问题?如何解决?
ABA问题是CAS的经典陷阱。线程1读取变量值为A,准备CAS修改为C;此时线程2将A改为B,又改回A;线程1执行CAS时,发现值还是A,修改成功。但实际上值已经被修改过两次,可能导致业务逻辑错误。
解决方案有三种:1)AtomicStampedReference使用版本号机制,每次修改版本号+1,CAS时同时检查值和版本号;2)AtomicMarkableReference使用布尔标记,适用于只需判断是否修改过的场景;3)使用互斥锁替代CAS,彻底避免ABA问题。实际项目中推荐第一种方案。
Q4:公平锁和非公平锁有什么区别?如何选择?
公平锁严格按照FIFO顺序获取锁,新线程必须排队等待;非公平锁允许新线程插队,直接尝试CAS获取锁,失败后才排队。公平锁保证公平,避免线程饥饿,但需要维护队列,性能较低;非公平锁性能高,减少上下文切换,但可能导致某些线程长时间获取不到锁。
选择建议:1)默认用非公平锁,性能更好;2)对公平性要求高的场景(如资源分配、任务调度)用公平锁;3)ReentrantLock默认是非公平锁,可通过构造函数指定fair=true创建公平锁。实际项目中,非公平锁能满足大多数场景。
Q5:如何避免死锁?死锁的四个必要条件是什么?
死锁的四个必要条件:1)互斥条件:资源一次只能被一个线程占用;2)占有并等待:线程持有资源同时等待其他资源;3)不可剥夺:资源不能强制抢占;4)循环等待:线程间形成循环等待关系。四个条件同时满足才会死锁。
避免死锁的策略:1)统一锁顺序:所有线程按相同顺序获取锁;2)使用tryLock超时获取:获取不到锁时释放已持有的锁;3)缩小锁范围:减少持锁时间;4)使用Lock的lockInterruptibly():响应中断打破死锁;5)使用工具检测:jstack、JConsole、Arthas都能检测死锁。最有效的是统一锁顺序。
参考资源
《Java并发编程实战》
《Java并发编程的艺术》
OpenJDK Wiki: Synchronization
JSR 133: Java Memory Model and Thread Specification
