深入拆解 synchronized:从偏向锁到重量级锁的升级之旅与优化秘籍

张开发
2026/4/10 11:19:12 15 分钟阅读

分享文章

深入拆解 synchronized:从偏向锁到重量级锁的升级之旅与优化秘籍
在Java并发编程中synchronized是最基础也最重要的同步工具之一。很多开发者只知道它能保证线程安全却对其底层实现一知半解。本文将深入JVM底层拆解synchronized的实现原理详解从偏向锁、轻量级锁到重量级锁的完整升级流程并介绍JVM为优化synchronized性能所做的努力。一、synchronized的基本用法synchronized可以保证在同一时刻只有一个线程能执行特定的代码块或方法。它主要有三种使用方式同步代码块、同步实例方法和同步静态方法。package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * synchronized基本用法示例 * * author ken */ Slf4j public class SynchronizedUsageDemo { private final Object lock new Object(); private static int staticCount 0; private int instanceCount 0; /** * 同步代码块对象锁 */ public void syncBlock() { synchronized (lock) { instanceCount; log.info(同步代码块执行instanceCount: {}, instanceCount); } } /** * 同步实例方法锁当前对象this */ public synchronized void syncInstanceMethod() { instanceCount; log.info(同步实例方法执行instanceCount: {}, instanceCount); } /** * 同步静态方法锁当前类的Class对象 */ public static synchronized void syncStaticMethod() { staticCount; log.info(同步静态方法执行staticCount: {}, staticCount); } public static void main(String[] args) { SynchronizedUsageDemo demo new SynchronizedUsageDemo(); new Thread(demo::syncBlock).start(); new Thread(demo::syncInstanceMethod).start(); new Thread(SynchronizedUsageDemo::syncStaticMethod).start(); new Thread(SynchronizedUsageDemo::syncStaticMethod).start(); } }二、Java对象头与Mark Word要理解synchronized的底层实现首先得了解Java对象在内存中的布局。Java对象由三部分组成对象头、实例数据和对齐填充。其中对象头是实现锁的关键它包含两部分信息Mark Word和Klass Pointer。 Mark Word用于存储对象自身的运行时数据如哈希码、GC分代年龄、锁状态标志等。在64位JVM中Mark Word的长度是64bit其结构会根据锁状态的不同而变化。锁状态Mark Word内容64bit无锁unused25bit| identity_hashcode31bit| unused1bit| age4bit| 0 | 01偏向锁thread54bit| epoch2bit| unused1bit| age4bit| 1 | 01轻量级锁ptr_to_lock_record62bit| 00重量级锁ptr_to_monitor62bit| 10GC标记empty62bit| 11我们可以通过JOLJava Object Layout工具来查看对象头的实际布局。dependency groupIdorg.openjdk.jol/groupId artifactIdjol-core/artifactId version0.17/version /dependencypackage com.jam.demo; import org.openjdk.jol.info.ClassLayout; /** * 查看对象头布局示例 * * author ken */ public class ObjectHeaderDemo { public static void main(String[] args) { Object obj new Object(); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); } }三、锁升级全流程JDK 1.6之后synchronized引入了锁升级机制以减少获取锁和释放锁带来的性能消耗。锁会从无锁开始随着竞争的加剧逐渐升级为偏向锁、轻量级锁最终升级为重量级锁。3.1 偏向锁当只有一个线程访问同步块时JVM会将锁偏向于该线程后续该线程再次访问时无需进行任何同步操作直接进入同步块。 偏向锁的获取流程线程访问同步块时检查Mark Word中的线程ID是否为空。如果为空通过CAS操作将Mark Word中的线程ID设置为当前线程ID此时进入偏向锁状态。如果线程ID已存在检查是否为当前线程ID如果是直接执行同步代码如果不是说明有其他线程竞争需要撤销偏向锁。 偏向锁的撤销需要等到全局安全点此时所有线程都停止执行字节码然后检查原持有偏向锁的线程是否存活如果原线程不存活将对象头重置为无锁状态然后重新偏向新线程。如果原线程存活升级为轻量级锁。3.2 轻量级锁当偏向锁被撤销或有多个线程交替使用锁时锁会升级为轻量级锁。轻量级锁使用CAS操作来尝试获取锁避免了重量级锁的线程阻塞和唤醒开销。 轻量级锁的加锁流程线程在自己的栈帧中创建一个名为Lock Record的结构用于存储Mark Word的副本。将对象头中的Mark Word复制到Lock Record中。通过CAS操作尝试将对象头的Mark Word替换为指向Lock Record的指针。如果CAS成功当前线程获得轻量级锁执行同步代码。如果CAS失败检查是否是锁重入如果是将Lock Record的displaced_header设为null作为重入的计数如果不是进行自适应自旋。如果自旋成功获得锁如果自旋失败升级为重量级锁。 轻量级锁的解锁流程通过CAS操作将Lock Record中的Mark Word复制回对象头。如果CAS成功解锁成功。如果CAS失败说明锁已经升级为重量级锁需要释放锁并唤醒等待的线程。3.3 重量级锁当锁竞争激烈轻量级锁的CAS操作和自旋都无法获取锁时锁会升级为重量级锁。重量级锁基于ObjectMonitor实现此时线程会被阻塞等待锁释放后被唤醒。 ObjectMonitor是JVM内部的一个对象包含以下关键字段_owner指向持有锁的线程。_EntryList存储等待获取锁的线程。_WaitSet存储调用wait()方法后等待的线程。 重量级锁的加锁流程线程进入_EntryList状态变为BLOCKED。当_owner线程释放锁时会从_EntryList中唤醒一个线程。被唤醒的线程尝试获取锁如果成功成为新的_owner。 重量级锁的解锁流程_owner线程释放锁将_owner设为null。从_EntryList中唤醒一个线程让它尝试获取锁。四、锁优化机制JVM为了提高synchronized的性能引入了多种锁优化机制包括自适应自旋、锁消除、锁粗化和逃逸分析。4.1 自适应自旋轻量级锁加锁失败时线程会进行自旋等待而不是直接阻塞。自适应自旋意味着自旋的时间不是固定的而是根据前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果前一次自旋成功获取了锁JVM会认为这次自旋也很可能成功从而延长自旋时间如果前一次自旋失败JVM会缩短自旋时间甚至直接跳过自旋。4.2 锁消除锁消除是指JIT编译器在运行时通过逃逸分析发现某个对象不会被其他线程访问那么该对象的锁就可以被消除。因为不会有线程竞争所以加锁和解锁操作是多余的。package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * 锁消除示例 * * author ken */ Slf4j public class LockEliminationDemo { /** * 方法中的StringBuffer是局部变量不会逃逸JIT会消除锁 */ public String concat(String a, String b, String c) { StringBuffer sb new StringBuffer(); sb.append(a); sb.append(b); sb.append(c); return sb.toString(); } public static void main(String[] args) { LockEliminationDemo demo new LockEliminationDemo(); for (int i 0; i 100000; i) { demo.concat(a, b, c); } log.info(执行完成); } }4.3 锁粗化锁粗化是指将连续的加锁和解锁操作合并成一个更大的锁范围减少加锁和解锁的次数。如果一个线程连续对同一个对象进行多次加锁和解锁JIT会将这些操作合并只进行一次加锁和解锁。package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * 锁粗化示例 * * author ken */ Slf4j public class LockCoarseningDemo { private final Object lock new Object(); private int count 0; /** * 连续的加锁解锁JIT会粗化锁范围 */ public void increment() { synchronized (lock) { count; } synchronized (lock) { count; } synchronized (lock) { count; } } public static void main(String[] args) { LockCoarseningDemo demo new LockCoarseningDemo(); for (int i 0; i 100000; i) { demo.increment(); } log.info(count: {}, demo.count); } }4.4 逃逸分析逃逸分析是JVM的一种分析技术用于判断对象的作用域是否会逃逸出方法或线程。如果对象不会逃逸JVM可以进行以下优化栈上分配将对象分配在栈上而不是堆上减少GC压力。标量替换将对象分解成多个基本类型直接分配在栈上。锁消除如前面所述消除不会逃逸对象的锁。五、实战死锁排查在使用synchronized时如果不注意加锁顺序可能会导致死锁。死锁是指两个或多个线程互相等待对方释放锁导致所有线程都无法继续执行。package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * 死锁示例 * * author ken */ Slf4j public class DeadlockDemo { private static final Object LOCK_A new Object(); private static final Object LOCK_B new Object(); public static void main(String[] args) { new Thread(() - { synchronized (LOCK_A) { log.info(线程1获取到LOCK_A); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(线程中断, e); } synchronized (LOCK_B) { log.info(线程1获取到LOCK_B); } } }, 线程1).start(); new Thread(() - { synchronized (LOCK_B) { log.info(线程2获取到LOCK_B); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(线程中断, e); } synchronized (LOCK_A) { log.info(线程2获取到LOCK_A); } } }, 线程2).start(); } }运行程序后我们可以通过jstack工具排查死锁用jps命令找到进程ID。用jstack 进程ID查看线程堆栈会看到明确的死锁提示。synchronized的底层实现涉及对象头、Mark Word、锁升级和各种优化机制。理解这些原理不仅能帮助我们写出更高效的并发代码还能在遇到并发问题时快速定位和解决。

更多文章