synchronized 底层全解:从对象头、锁升级到内核实现,击穿并发编程的核心基石

synchronized 底层全解:从对象头、锁升级到内核实现,击穿并发编程的核心基石 在Java并发编程体系中synchronized是解决多线程安全问题的原生基石也是面试与生产环境中的核心考点。很多开发者仅停留在“会用”的层面对其底层实现一知半解最终导致生产环境出现死锁、性能瓶颈、线程安全故障时无从排查。本文将从内存布局、锁升级全流程、JVM内核实现、实战验证四个维度用通俗的语言彻底讲透synchronized的底层逻辑兼顾理论深度与生产实用性。一、synchronized 基础认知与使用范式synchronized是Java语言内置的互斥同步锁基于JVM虚拟机实现能够保证多线程场景下共享资源的原子性、可见性与有序性同时具备自动释放锁、可重入的特性是Java并发编程中最常用的线程安全保障手段。1.1 三种核心使用范式synchronized的锁粒度由锁对象决定根据锁对象的不同分为三种标准使用方式不同方式的锁范围与作用场景有明确区别。package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * synchronized 三种使用范式示例 * author ken */ Slf4j public class SynchronizedUsageDemo { /** * 共享计数变量 */ private static int staticCount 0; private int instanceCount 0; private final Object lockObject new Object(); /** * 1. 修饰实例方法锁对象为当前实例this * 作用范围当前实例的所有同步实例方法不同实例之间互不影响 */ public synchronized void instanceMethodIncrement() { instanceCount; log.info(实例方法计数:{}, instanceCount); } /** * 2. 修饰静态方法锁对象为当前类的Class对象 * 作用范围当前类的所有同步静态方法全局唯一所有实例共享 */ public static synchronized void staticMethodIncrement() { staticCount; log.info(静态方法计数:{}, staticCount); } /** * 3. 修饰代码块锁对象为自定义配置的对象 * 作用范围代码块内部锁粒度最细灵活性最高 */ public void codeBlockIncrement() { // 锁对象为自定义的lockObject仅锁定代码块内的逻辑 synchronized (lockObject) { instanceCount; log.info(代码块计数:{}, instanceCount); } } public static void main(String[] args) { SynchronizedUsageDemo demo new SynchronizedUsageDemo(); // 多线程测试实例方法 for (int i 0; i 10; i) { new Thread(demo::instanceMethodIncrement).start(); } // 多线程测试静态方法 for (int i 0; i 10; i) { new Thread(SynchronizedUsageDemo::staticMethodIncrement).start(); } // 多线程测试代码块 for (int i 0; i 10; i) { new Thread(demo::codeBlockIncrement).start(); } } }1.2 字节码层面的实现原理synchronized的底层实现基于JVM的管程Monitor模型在字节码层面分为两种实现形式同步代码块通过monitorenter和monitorexit字节码指令实现。编译器会自动生成两个monitorexit指令分别对应正常执行路径与异常执行路径确保无论代码是否抛出异常锁都能被正确释放避免死锁。同步方法通过方法访问标志ACC_SYNCHRONIZED隐式实现。线程调用方法时会先检查方法是否携带该标志若携带则自动获取对应对象的管程方法执行完成后自动释放管程执行过程中出现异常同样会自动释放锁。二、synchronized 底层基石对象内存布局与对象头synchronized的锁信息完全存储在锁对象的对象头中想要理解锁的底层逻辑必须先搞清楚Java对象在堆内存中的完整布局。2.1 Java对象的内存布局在HotSpot虚拟机中一个Java对象在堆内存中的存储结构分为三个固定部分整体结构如下实例数据存储对象的成员变量包括父类继承的成员变量按照数据类型长度对齐排列。对齐填充HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍当对象整体长度不足8字节的整数倍时通过对齐填充补全仅起到占位作用无实际逻辑意义。对象头synchronized实现锁的核心载体分为三个子部分Klass Pointer类型指针对象指向其类元数据的指针64位虚拟机默认开启指针压缩-XX:UseCompressedOops后占用4字节关闭后占用8字节JVM通过该指针确定对象所属的类。数组长度仅数组对象拥有占用4字节存储数组的长度非数组对象无该部分。Mark Word标记字段占用8字节64位是整个synchronized锁实现的核心存储了对象的哈希码、分代年龄、锁状态标志、线程ID、偏向时间戳等核心信息锁的升级过程本质上就是Mark Word中存储内容与状态的切换过程。2.2 Mark Word 的状态与结构64位虚拟机中Mark Word是一个动态的数据结构会根据锁状态的不同复用64个bit位以此节省内存开销。不同锁状态下的bit位分配如下表所示锁状态偏向锁位锁标志位64bit位存储内容从高位到低位无锁状态001未使用(25bit)偏向锁状态101线程ID(54bit)轻量级锁状态无00指向当前线程栈中锁记录(Lock Record)的指针(62bit)重量级锁状态无10指向重量级锁ObjectMonitor对象的指针(62bit)GC标记状态无11空(62bit)核心要点说明无锁与偏向锁的锁标志位均为01通过偏向锁位区分两种状态不同锁状态下Mark Word的核心存储内容完全不同锁升级的过程就是通过CAS操作修改Mark Word的内容与标志位分代年龄占用4bit最大值为15这也是JVM中对象默认晋升到老年代的年龄阈值为15的根本原因。三、synchronized 核心机制锁升级全流程锁升级的核心设计思想是尽可能避免重量级锁带来的用户态与内核态切换开销JVM会根据锁的竞争激烈程度从低到高逐步升级锁的级别而非一上来就使用开销巨大的重量级锁。锁升级的完整路径为无锁 → 偏向锁需手动开启 → 轻量级锁 → 重量级锁锁的升级过程是单向不可逆的只能从低级别向高级别升级无法降级但锁完全释放后对象会回到无锁状态下一次加锁会重新触发锁升级流程。3.1 偏向锁偏向锁的核心设计目标是消除无竞争场景下的所有同步开销连CAS操作都完全省略。其核心逻辑是当锁第一次被线程获取时会通过CAS操作将线程ID记录到对象的Mark Word中之后该线程再次进入同步块时无需任何同步操作直接进入彻底消除无竞争场景下的同步开销。3.1.1 偏向锁的开启与适用场景JDK15及之后版本中JEP 374已经默认禁用偏向锁并将其标记为废弃状态如需使用需手动添加JVM启动参数-XX:UseBiasedLocking -XX:BiasedLockingStartupDelay0偏向锁仅适用于单线程反复进入同步块、完全无竞争的场景例如单线程环境下使用线程安全的工具类一旦出现多线程竞争偏向锁会立即触发撤销带来额外的性能开销。3.1.2 偏向锁的获取流程线程进入同步块时先检查锁对象Mark Word的偏向锁位是否为1、锁标志位是否为01确认对象处于可偏向状态若处于可偏向状态检查Mark Word中存储的线程ID是否为当前线程ID若是则直接进入同步块无需任何CAS操作若线程ID不是当前线程ID通过CAS操作尝试将Mark Word中的线程ID替换为当前线程ID若CAS成功当前线程获取偏向锁成功进入同步块执行若CAS失败说明存在多线程竞争立即触发偏向锁撤销流程。3.1.3 偏向锁的撤销偏向锁的撤销必须在全局安全点Safe Point执行该点所有用户线程都会暂停具体流程如下暂停所有用户线程检查持有偏向锁的线程是否仍处于存活状态若持有偏向锁的线程已经退出同步块或已经死亡将锁对象的Mark Word恢复为无锁可偏向状态唤醒用户线程继续执行若持有偏向锁的线程仍处于同步块中将偏向锁直接升级为轻量级锁将Mark Word更新为指向持有线程栈中锁记录的指针唤醒用户线程继续执行。3.2 轻量级锁轻量级锁的核心设计目标是在多线程交替执行同步块、竞争不激烈的场景下通过CAS操作避免重量级锁的内核态切换开销其核心实现基于线程栈帧中的锁记录Lock Record。3.2.1 轻量级锁的加锁流程线程进入同步块时若锁对象处于无锁状态偏向锁位0锁标志位01JVM会在当前线程的栈帧中创建一块名为锁记录Lock Record的内存空间用于存储锁对象当前Mark Word的完整拷贝该拷贝被称为Displaced Mark WordJVM通过CAS操作尝试将锁对象的Mark Word更新为指向当前线程Lock Record的指针若CAS成功当前线程获取轻量级锁成功将锁对象的锁标志位设置为00进入同步块执行若CAS失败JVM会先检查锁对象的Mark Word是否已经指向当前线程栈帧中的Lock Record若是则说明是当前线程的锁重入直接进入同步块执行若不是重入场景说明存在其他线程竞争锁当前线程会通过自适应自旋的方式反复CAS尝试获取锁若自旋达到阈值仍未获取到锁轻量级锁立即升级为重量级锁锁标志位修改为10Mark Word中存储指向重量级锁ObjectMonitor对象的指针当前线程停止自旋进入阻塞状态。3.2.2 轻量级锁的解锁流程线程退出同步块时通过CAS操作尝试将栈帧中存储的Displaced Mark Word替换回锁对象的Mark Word中若CAS成功解锁成功锁对象恢复为无锁状态若CAS失败说明锁在持有过程中已经升级为重量级锁进入重量级锁的解锁流程唤醒阻塞等待的线程。3.2.3 自适应自旋JDK1.6之后引入的自适应自旋机制彻底解决了固定自旋次数的弊端自旋次数不再固定由JVM根据前一次在同一个锁上的自旋结果与锁的持有状态动态调整若同一个锁对象上自旋刚刚成功获取到锁且持有锁的线程正在运行JVM会认为本次自旋成功的概率极高自动增加自旋次数若同一个锁对象上自旋很少成功获取到锁JVM会直接减少自旋次数甚至完全跳过自旋流程直接升级为重量级锁避免CPU空转带来的性能浪费。3.3 重量级锁重量级锁是synchronized的最终形态适用于多线程同时竞争锁、竞争激烈、锁持有时间长的场景其底层基于操作系统的互斥量mutex实现会涉及用户态与内核态的切换开销远高于轻量级锁与偏向锁。3.3.1 重量级锁的核心载体ObjectMonitorHotSpot虚拟机中每个对象都对应一个C实现的ObjectMonitor对象这是重量级锁的核心载体其核心结构如下class ObjectMonitor { private: volatile Thread* _owner; // 持有当前锁的线程 volatile int _count; // 锁计数器获取锁1释放锁-1 int _recursions; // 锁的重入次数 ObjectWaiter* _entryList; // 竞争锁失败的线程阻塞队列 ObjectWaiter* _waitSet; // 调用wait方法的线程等待队列 // 其他优化字段与方法省略 };3.3.2 重量级锁的加锁流程线程竞争锁时先检查ObjectMonitor的_owner字段是否为null若是则将_owner设置为当前线程_count加1获取锁成功若_owner已经指向当前线程说明是锁重入将_recursions加1_count加1直接进入同步块执行若_owner指向其他线程当前线程会进入_entryList队列通过操作系统的互斥量阻塞等待直到持有锁的线程释放锁后被唤醒重新竞争锁。3.3.3 重量级锁的解锁流程线程退出同步块时将_recursions减1_count减1当_recursions减至0时说明锁已经完全释放将_owner设置为null唤醒_entryList中阻塞等待的线程重新竞争锁重量级锁的竞争是非公平的新到来的线程会先尝试抢占锁抢占失败才会进入_entryList队列而非直接进入队列排队。3.3.4 wait/notify/notifyAll的底层实现这三个方法是Object类的原生方法必须在synchronized同步块中调用否则会抛出IllegalMonitorStateException异常其底层完全基于ObjectMonitor实现wait方法线程调用该方法时会释放持有的锁将_recursions和_count清零_owner设置为null线程进入_waitSet队列阻塞等待直到被notify/notifyAll唤醒notify方法线程调用该方法时会从_waitSet队列中随机唤醒一个线程将其转移到_entryList队列中等待竞争锁notifyAll方法线程调用该方法时会唤醒_waitSet队列中的所有线程全部转移到_entryList队列中等待竞争锁。四、JVM 对 synchronized 的额外优化除了锁升级机制JVM还提供了三种经典的锁优化手段进一步降低synchronized的性能开销所有优化均在JIT编译阶段触发默认全部开启。4.1 锁消除锁消除是指JIT编译器在动态编译时通过逃逸分析检测到同步块的锁对象不会发生逃逸不可能被多个线程访问不存在竞争就会直接消除该同步块的锁逻辑无需任何同步操作。开启参数-XX:EliminateLocks默认开启package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * 锁消除示例 * author ken */ Slf4j public class LockEliminationDemo { /** * 锁对象为方法内局部变量不会发生逃逸不存在多线程竞争 * JIT会自动消除该同步块的锁逻辑 */ public void lockEliminationTest() { // 锁对象为局部变量栈私有不会被其他线程访问 Object lockObject new Object(); synchronized (lockObject) { int sum 0; for (int i 0; i 100; i) { sum i; } log.info(计算结果:{}, sum); } } public static void main(String[] args) { LockEliminationDemo demo new LockEliminationDemo(); for (int i 0; i 10000; i) { demo.lockEliminationTest(); } } }上述示例中锁对象是方法内的局部变量不会发生逃逸JIT会直接消除synchronized锁逻辑执行时不会有任何同步开销。4.2 锁粗化锁粗化是指JIT编译器检测到一系列连续的操作都对同一个对象反复加锁解锁会将锁的范围粗化到整个操作序列的外部减少频繁加锁解锁带来的性能开销最典型的场景就是循环内的加锁操作。开启参数-XX:EliminateAllocations默认开启package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * 锁粗化示例 * author ken */ Slf4j public class LockCoarseningDemo { private int count 0; private final Object lockObject new Object(); /** * 循环内反复加锁解锁JIT会自动将锁粗化到循环外部 */ public void lockCoarseningTest() { for (int i 0; i 100; i) { synchronized (lockObject) { count; } } log.info(计数结果:{}, count); } public static void main(String[] args) { LockCoarseningDemo demo new LockCoarseningDemo(); demo.lockCoarseningTest(); } }上述示例中循环内每次迭代都会加锁解锁JIT会自动将锁粗化到循环外部整个循环只需要一次加锁解锁操作。五、实战验证用JOL查看对象头与锁升级过程通过OpenJDK官方的JOLJava Object Layout工具可以直观地查看不同锁状态下对象头的变化验证锁升级的完整流程。5.1 环境依赖配置?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.jam/groupId artifactIdsynchronized-demo/artifactId version1.0-SNAPSHOT/version properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.34/version scopeprovided/scope /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version2.0.13/version /dependency dependency groupIdch.qos.logback/groupId artifactIdlogback-classic/artifactId version1.5.6/version /dependency dependency groupIdorg.openjdk.jol/groupId artifactIdjol-core/artifactId version0.17/version /dependency /dependencies /project5.2 锁升级过程验证代码package com.jam.demo; import lombok.extern.slf4j.Slf4j; import org.openjdk.jol.info.ClassLayout; /** * 锁升级过程验证 * author ken */ Slf4j public class LockUpgradeVerifyDemo { /** * 锁对象 */ private static final Object LOCK_OBJECT new Object(); public static void main(String[] args) throws InterruptedException { log.info( 1. 无锁状态 ); log.info(ClassLayout.parseInstance(LOCK_OBJECT).toPrintable()); log.info( 2. 单线程加锁轻量级锁状态 ); synchronized (LOCK_OBJECT) { log.info(ClassLayout.parseInstance(LOCK_OBJECT).toPrintable()); } log.info( 3. 锁释放后回到无锁状态 ); log.info(ClassLayout.parseInstance(LOCK_OBJECT).toPrintable()); log.info( 4. 多线程竞争升级为重量级锁 ); // 线程1持有锁3秒 Thread thread1 new Thread(() - { synchronized (LOCK_OBJECT) { try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(线程中断, e); } } }); // 线程2同时竞争锁触发锁升级 Thread thread2 new Thread(() - { synchronized (LOCK_OBJECT) { log.info(线程2获取到锁); } }); thread1.start(); // 确保线程1先启动并持有锁 Thread.sleep(500); thread2.start(); // 等待线程竞争完成 Thread.sleep(4000); log.info( 5. 竞争完成后重量级锁状态 ); synchronized (LOCK_OBJECT) { log.info(ClassLayout.parseInstance(LOCK_OBJECT).toPrintable()); } } }5.3 运行结果说明无锁状态对象头的锁标志位为01偏向锁位为0存储了对象的identity哈希码与分代年龄轻量级锁状态单线程加锁后锁标志位变为00Mark Word中存储了指向当前线程栈中锁记录的指针锁释放后轻量级锁解锁完成对象头恢复为无锁状态锁标志位回到01重量级锁状态多线程竞争后锁标志位变为10Mark Word中存储了指向ObjectMonitor对象的指针锁升级完成。六、常见误区与核心问题辨析6.1 误区1锁升级是不可逆的一旦升级为重量级锁就永远是重量级锁纠正锁升级的过程是单向的只能从低级别向高级别升级无法降级但当重量级锁完全释放后锁对象的Mark Word会恢复为无锁状态下一次加锁会重新从无锁开始触发锁升级流程而非直接使用重量级锁。6.2 误区2synchronized是公平锁纠正synchronized整体是非公平锁。虽然重量级锁会按照FIFO顺序唤醒_entryList中的线程但新到来的线程会先尝试抢占锁抢占失败才会进入_entryList队列存在插队的可能因此整体是非公平的无法保证等待时间最长的线程优先获取锁。6.3 误区3synchronized等待锁时可以响应中断纠正synchronized在等待锁的过程中无法响应中断。调用线程的interrupt()方法不会中断正在等待锁的线程只有当线程获取到锁之后才会响应中断。而java.util.concurrent.locks.Lock接口的lockInterruptibly()方法可以在等待锁的过程中响应中断。6.4 误区4JDK17默认开启偏向锁纠正JDK15及之后版本JEP 374已经默认禁用偏向锁并将其标记为废弃状态JDK17中需要手动添加JVM参数才能开启偏向锁生产环境不建议使用已废弃的偏向锁特性。6.5 误区5wait/notify可以在同步块之外调用纠正wait/notify/notifyAll方法必须在synchronized同步块内部调用因为调用这些方法需要先持有对象的管程Monitor否则会直接抛出IllegalMonitorStateException异常这是JVM规范强制要求的。七、生产环境最佳实践缩小同步块范围仅将必须同步的核心代码放入synchronized块中减少锁的持有时间降低竞争概率避免锁升级为重量级锁。锁对象私有化与不可变锁对象必须声明为private final避免锁对象被外部修改导致加锁与解锁的对象不一致引发线程安全问题。禁止使用基础类型包装类、字符串常量作为锁对象字符串常量池、包装类的缓存机制会导致锁对象被全局共享极易引发意想不到的死锁与线程安全问题。避免在同步块中执行耗时操作不要在synchronized同步块中调用阻塞方法、IO操作、wait方法避免锁持有时间过长导致竞争加剧锁升级为重量级锁。优先使用synchronized而非手动锁JDK17中synchronized已经经过深度优化性能与ReentrantLock差距极小且无需手动释放锁不会出现忘记解锁导致的死锁问题代码更简洁维护成本更低。仅在需要公平锁、可中断锁、超时锁等高级特性时再使用ReentrantLock。避免锁对象的过度共享尽量降低锁对象的共享范围例如使用细粒度锁将一个大锁拆分为多个小锁降低竞争的概率提升并发性能。