2026/8/18 20:12:59

深入剖析Java synchronized底层原理:从Monitor到锁升级

深入剖析Java synchronized底层原理:从Monitor到锁升级 一、前言在JDK 1.6之前synchronized一直被叫作重量级锁。因为它的实现直接依赖操作系统的Mutex Lock互斥锁。线程获取锁失败会被阻塞唤醒又需要恢复中间涉及用户态到内核态的切换开销非常大。但从JDK 1.6开始Java对synchronized做了一系列优化引入了偏向锁和轻量级锁还加入了锁升级机制。锁不再一上来就是重量级的而是根据竞争激烈程度自适应地升级。这就好比一个停车场· 刚开始车少随便停偏向锁· 车多起来管理员引导一下但不用抬杆轻量级锁自旋· 真堵死了抬杆排队一个一个进重量级锁下面咱们就从底层的Monitor开始一步步把synchronized的原理讲清楚。二、Monitor —— synchronized的底层管家2.1 什么是MonitorMonitor翻译过来叫监视器或管程。每个Java对象都可以关联一个Monitor当线程要进入synchronized代码块时本质上就是在尝试获取这个对象关联的Monitor的所有权。在HotSpot虚拟机中Monitor由C的ObjectMonitor类实现。它的核心结构如下ObjectMonitor -------------------- | _owner | (当前持有锁的线程) | _count | (计数器支持可重入) | _entryList | (入口阻塞队列) | _waitSet | (调用wait()的等待集合) --------------------2.2 字节码层面的实现synchronized在字节码层面主要通过两条指令完成· monitorenter进入同步块尝试获取Monitor· monitorexit退出同步块释放Monitor编译器会在同步代码块入口插入monitorenter在正常退出和异常退出的路径各插入一个monitorexit确保锁一定会被释放。而synchronized修饰的方法则通过方法上的ACC_SYNCHRONIZED标志来识别。2.3 加锁与释放过程线程执行monitorenter时流程如下检查_count是否为0若为0说明锁未被占用将_owner设为当前线程_count加1若_count不为0且_owner就是当前线程_count再加1可重入若_count不为0且_owner是别的线程当前线程进入_entryList阻塞等待执行monitorexit时_count减1减到0时释放锁并唤醒_entryList中的等待线程。三、对象头 —— 锁信息的身份证Monitor和对象是怎么关联起来的全靠对象头。Java对象在内存中分为三部分对象头、实例数据、对齐填充。对象头里的Mark Word是关键它存储了对象的哈希码、GC分代年龄以及最重要的锁状态信息。Mark Word会根据锁状态复用存储空间锁状态 Mark Word存储内容 锁标志位无锁 对象哈希码、分代年龄 01偏向锁 线程ID 01轻量级锁 指向栈中锁记录的指针 00重量级锁 指向Monitor的指针 10锁升级的过程本质上就是Mark Word中锁标志位和存储内容不断变化的过程。四、锁升级全过程含图示锁的升级路径是无锁 → 偏向锁 → 轻量级锁 → 重量级锁。这个升级是单向不可逆的——锁只能从低级别升到高级别不能降级。4.1 无锁状态对象刚创建、还没有任何线程来竞争时处于无锁状态。Mark Word里存的是对象的哈希码和分代年龄锁标志位为01但偏向标志位为0。4.2 偏向锁 —— “这个位置我占了”适用场景自始至终只有一个线程在访问同步块。原理当第一个线程Thread-0进入同步块时JVM通过CAS操作把当前线程的ID写入Mark Word。之后这个线程再进入同步块时只需检查Mark Word里的线程ID是不是自己即可不需要任何加锁解锁操作几乎零开销。下面的图示展示了偏向锁下的锁重入场景——线程0依次调用m1、m2、m3三个同步方法每次进入时只需要检查线程id是否是自己即可无需额外操作。偏向锁获取与重入图解static final Object obj new Object(); public static void m1() { synchronized (obj) { // 同步块A m2(); } } public static void m2() { synchronized (obj) { // 同步块B m3(); } } public static void m3() { synchronized (obj) { // 同步块C // do something } } Thread-0 (首次进入以及后续重入) | | 第一次进入m1: CAS (将线程ID写入对象头) | 后续进入m2/m3: 直接检查 线程id是否是自己 V ------------------------------------------------------ | Mark Word (对象头) | 锁标志位: 01 (偏向) | [ thread-id | age 1 | 01 ] | | (线程ID age1 偏向标志) | ------------------------------------------------------ | Klass Word | (指向类的元数据) ------------------------------------------------------ | Object Body | (实例数据) ------------------------------------------------------ | | 关联 V -------------------------------- | Thread-0 的栈帧 | | ---------------------------- | | | Lock Record (偏向锁不占用) | | --- 偏向锁下Lock Record为null | | null | | | ---------------------------- | | | Object reference | | --- 指向Object的引用 | ---------------------------- | --------------------------------重点偏向锁下Lock Record里是null因为压根不需要额外的锁记录。每次重入只需比对对象头中的线程ID比对成功直接进入开销极小。升级条件当有其他线程尝试竞争这个锁时偏向锁会被撤销升级为轻量级锁。4.3 轻量级锁 —— “商量着来”适用场景多个线程交替访问同步块没有真正的竞争比如线程0刚用完线程1才来。原理当第二个线程来竞争时偏向锁撤销升级为轻量级锁。JVM会在当前线程的栈帧中创建一个锁记录Lock Record然后通过CAS操作尝试把Mark Word更新为指向这个锁记录的指针。下面的图示展示了轻量级锁下的锁重入场景——线程0在持有锁的情况下再次进入method2的同步块同一线程重入JVM会在栈中创建新的Lock Record但Mark Word依然指向第一个Lock Record。轻量级锁获取与重入图解final Object obj new Object(); public static void method1() { synchronized (obj) { // 同步块A method2(); } } public static void method2() { synchronized (obj) { // 同步块B // do something } } Thread-0 (持有锁并发生重入) | | method1: CAS (将对象头的指针指向自己栈中第一个Lock Record) | method2: 重入再创建一个Lock Record但对象头仍然指向第一个 V ------------------------------------------------------ | Mark Word (对象头) | 锁标志位: 00 (轻量级) | [ Lock record 地址 (指向线程0栈中LR1) ] | | | ------------------------------------------------------ | Klass Word | ------------------------------------------------------ | Object Body | ------------------------------------------------------ ^ | 对象头中的指针指向 | ------------------------------------------------------- | Thread-0 的栈帧 | | ---------------------------- | | | Lock Record 1 (LR1) | --- 第一个Lock Record | | | [ 指向Object的引用 ] | | | | [ 锁记录地址指向对象头 ] | | | ---------------------------- | | ---------------------------- | | | Lock Record 2 (LR2) | --- 重入时创建内容为null | | | [ null ] | | | ---------------------------- | ------------------------------------------------------ | | Object reference (指向Object) V ------------------------------------------------------ | Object对象 (与上面是同一个) | ------------------------------------------------------关键点· 首次获取CAS将对象头的Mark Word更新为指向栈中Lock Record 1的地址。· 锁重入线程0再次进入method2时会在栈中再创建一个Lock Record 2内容为null表示重入。对象头的Mark Word仍然指向LR1不会变。· 释放锁退出同步块时依次将Lock Record出栈。当最后一个非null的Lock Record即LR1出栈时通过CAS将对象头恢复为无锁状态。· CAS失败如果有别的线程来竞争并且CAS更新对象头失败说明锁已被占用当前线程会自旋等待反复尝试不阻塞。自旋在用户态进行不涉及内核切换。升级条件自旋超过一定次数仍然获取不到锁或者多个线程同时激烈竞争时升级为重量级锁。4.4 重量级锁 —— “排队进场”适用场景多个线程同时激烈竞争同一把锁。原理升级为重量级锁后Mark Word中存储指向操作系统Monitor对象的指针。没抢到锁的线程会进入_entryList阻塞真正让出CPU。图解重量级锁状态及Monitor结构Thread-1 Thread-2 Thread-3 (多个线程同时竞争) \ | / \ | / V V V ------------------------------------------------------ | Mark Word (对象头) | 锁标志位: 10 (重量级) | [ 指向 ObjectMonitor 的指针 ] | ------------------------------------------------------ | Klass Word | ------------------------------------------------------ | Object Body | ------------------------------------------------------ | | (关联) V ----------------------- | ObjectMonitor | | ----------------- | | | _owner | | (当前持有锁的线程 Thread-0) | | _count 1 | | (重入次数) | | _entryList | | (阻塞队列: Thread-1, Thread-2 排队等锁) | | _waitSet | | (等待集: 调用了wait()的线程) | ----------------- | -----------------------重量级锁的缺点是线程阻塞和唤醒涉及用户态与内核态的切换性能开销最大。但优点是不会持续占用CPU适合高竞争、锁持有时间长的场景。五、其他JVM锁优化除了锁升级JVM还做了这些优化· 自适应自旋锁轻量级锁阶段线程不是死板地自旋固定次数。JVM会根据上一次自旋的成功率动态调整自旋次数成功率越高就多转几次成功率低就早点阻塞。· 锁消除JIT编译器通过逃逸分析发现某个锁对象只可能被一个线程访问时会直接把锁去掉比如局部变量加锁。· 锁粗化如果连续多次对同一个对象加锁解锁比如循环里的加锁JVM会把它们合并成一次大的加锁减少获取/释放锁的次数。六、总结最后用一张总览图回顾整个流程无锁 (01,存HashCode) | | (第一个线程进入) V 偏向锁 (01,存线程ID) --- 几乎没有开销重入只检查线程ID | | (有线程来竞争) V 轻量级锁 (00,存Lock Record指针) --- CAS自旋用户态操作重入创建LR但指针不变 | | (竞争加剧/自旋失败) V 重量级锁 (10,存Monitor指针) --- 阻塞唤醒涉及内核切换靠ObjectMonitor管理核心要记住的几点底层核心是Monitor所有锁的最终归宿都是ObjectMonitor。状态记录在Mark Word锁升级就是Mark Word内容的变更。升级是单向的只会从偏向→轻量→重量不会降级。可重入性偏向锁靠比对线程ID轻量锁靠栈中Lock Record的计数重量锁靠_count计数器。锁重入时轻量级锁会在栈中创建多个Lock Record但对象头始终指向第一个非空的LR。