synchronized关键字的底层实现(从JVM到字节码+硬件,全维度解析)
synchronized是Java原生的重量级锁 (JDK1.6后做了大量优化,成为偏向锁→轻量级锁→重量级锁 的自适应锁),其底层实现依托JVM的字节码指令 和操作系统的同步原语 ,同时结合对象头的锁状态标记 实现锁的升级与降级,是Java并发中保证原子性、可见性、有序性的核心关键字。
本文从字节码层面 、对象头层面 、锁状态升级层面 、重量级锁底层四层拆解其实现原理,兼顾基础概念与底层细节,覆盖面试高频考点。
一、先明确核心定位
synchronized是可重入的互斥锁 ,支持修饰实例方法、静态方法、代码块 ,不同修饰方式对应不同的锁对象(此前已讲,此处快速回顾,为底层实现铺垫):
- 修饰非静态方法 :锁对象为当前实例对象(this);
- 修饰静态方法 :锁对象为当前类的Class对象;
- 修饰代码块 :锁对象为括号内的自定义对象 (如
synchronized(obj){}锁的是obj)。
底层核心逻辑 :JVM通过对锁对象的对象头 进行状态标记,结合字节码指令实现锁的获取与释放;同时根据竞争程度自动实现锁的升级(偏向→轻量→重量),减少锁竞争的性能损耗。
二、字节码层面:synchronized的基础实现
synchronized的最外层实现体现在Java字节码指令 中,JVM为其提供了**monitorenter和 monitorexit两个核心指令,分别对应 锁的获取和锁的释放**。
1. 修饰代码块的字节码实现
以简单的同步代码块为例,看其编译后的字节码:
java
public class SyncDemo {
private Object obj = new Object();
public void test() {
synchronized (obj) { // 同步代码块
System.out.println("synchronized code");
}
}
}
编译后对obj加锁的核心字节码指令:
// 先获取obj对象的引用,压入操作数栈
aload_1
// 进入同步监视器:获取obj的锁,对应monitorenter
monitorenter
// 执行同步代码块逻辑(打印语句)
...
// 退出同步监视器:释放锁,对应monitorexit
monitorexit
// 异常场景的monitorexit(保证锁一定释放)
monitorexit
核心细节
monitorenter:执行该指令时,当前线程会尝试获取锁对象对应的监视器(Monitor) 的所有权:- 若监视器的进入数为0,线程成功获取,将进入数设为1;
- 若当前线程已持有该监视器,进入数+1(可重入的实现);
- 若其他线程持有监视器,当前线程阻塞,直到监视器被释放。
monitorexit:执行该指令时,线程将监视器的进入数-1;当进入数减为0时,释放监视器,阻塞的线程可重新竞争锁。- 两个
monitorexit:JVM会在正常执行路径和异常执行路径各插入一个monitorexit,保证无论是否发生异常,锁都会被释放,避免死锁。
2. 修饰方法的字节码实现
synchronized修饰方法时,不会生成monitorenter/monitorexit指令 ,而是通过方法的访问标志 实现:
JVM为方法提供了ACC_SYNCHRONIZED访问标志,当方法被该标志标记时,表明此方法为同步方法。
核心逻辑
- 线程调用该方法时,会先尝试获取方法对应的锁对象的监视器;
- 获取成功则执行方法体,执行完毕后释放监视器;
- 获取失败则线程阻塞,直到监视器被释放。
字节码标识示例
// 同步非静态方法:ACC_PUBLIC + ACC_SYNCHRONIZED
public synchronized void test1();
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
// 同步静态方法:ACC_PUBLIC + ACC_STATIC + ACC_SYNCHRONIZED
public static synchronized void test2();
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
3. 核心共性:监视器(Monitor)
无论是同步代码块还是同步方法,底层都依赖监视器(Monitor) ,其本质是操作系统的互斥量(Mutex) ,是一个ObjectMonitor对象(C++实现),核心属性包括:
_owner:持有当前监视器的线程;_EntryList:阻塞等待获取监视器的线程队列;_WaitSet:调用wait()后等待的线程队列;_count:监视器的进入数(可重入计数)。
Monitor的核心作用:作为锁的底层载体,实现线程的互斥与阻塞。
三、对象头层面:锁状态的存储载体
synchronized的锁状态并非存储在锁对象本身 ,而是存储在锁对象的对象头 中,Java中所有对象的对象头是实现synchronized的核心数据结构。
1. Java对象的内存布局
Java对象在堆内存中的布局分为3部分,对象头是核心:
对象 = 对象头(Header) + 实例数据(Instance Data) + 对齐填充(Padding)
2. 对象头的结构
对象头的结构随JVM位数(32/64位)略有差异,64位JVM(开启指针压缩) 为生产环境主流,其结构如下:
| 部分 | 大小 | 核心作用 |
|---|---|---|
| Mark Word | 64bit | 存储锁状态、线程ID、哈希值等核心信息(锁的核心载体) |
| Klass Pointer | 32bit | 指向对象所属类的Class对象(指针压缩后) |
| 数组长度 | 32bit | 仅数组对象有,记录数组长度 |
核心重点 :Mark Word(标记字段) 是实现synchronized锁状态的关键,它是一个动态变化的字段 ,会根据锁的竞争程度 改变存储内容,实现锁状态的切换。
3. Mark Word的动态存储结构(64位JVM)
Mark Word采用内存复用技术,不同锁状态下存储不同的信息,默认无锁状态的存储结构为:
无锁状态:哈希值(31bit) + 分代年龄(4bit) + 是否是偏向锁(1bit=0) + 锁状态(2bit=01)
当对象被作为synchronized的锁对象时,Mark Word会根据锁状态更新,不同锁状态对应不同的Mark Word存储内容,核心锁状态的Mark Word结构:
| 锁状态 | 锁标志位(2bit) | 偏向锁标志(1bit) | Mark Word存储内容 |
|---|---|---|---|
| 无锁 | 01 | 0 | 哈希值 + 分代年龄 + 0 + 01 |
| 偏向锁 | 01 | 1 | 持有锁的线程ID + Epoch + 分代年龄 +1+01 |
| 轻量级锁 | 00 | - | 指向线程栈中锁记录的指针 + 00 |
| 重量级锁 | 10 | - | 指向堆中ObjectMonitor对象的指针 +10 |
| GC标记 | 11 | - | 空(仅用于GC标记) |
核心逻辑 :JVM通过修改Mark Word的锁标志位 和存储内容,标记当前对象的锁状态,同时记录持有锁的线程/监视器信息,实现锁的管理。
四、锁状态升级:从偏向锁到重量级锁(JDK1.6核心优化)
JDK1.6之前,synchronized是纯重量级锁 ,直接依赖操作系统的Mutex,线程阻塞/唤醒会触发内核态与用户态的切换 ,性能极低;
JDK1.6后,JVM为synchronized做了锁膨胀 优化,引入偏向锁、轻量级锁 ,只有当锁竞争激烈 时,才会升级为重量级锁 ,实现自适应的锁策略。
核心原则
- 锁升级是单向的 :偏向锁 → 轻量级锁 → 重量级锁,不能降级(保证锁的性能最优);
- 锁的选择基于竞争程度:无竞争→偏向锁,轻度竞争→轻量级锁,重度竞争→重量级锁;
- 所有锁都是可重入的:基于Mark Word/Monitor的计数实现。
1. 偏向锁:无竞争场景的最优解
适用场景 :一个线程多次获取同一把锁 ,无任何其他线程竞争,是单线程执行同步代码的最优锁状态。
核心设计 :锁偏向于第一个获取它的线程 ,该线程后续再次获取锁时,无需任何竞争操作,直接获取,避免锁竞争的开销。
实现原理
- 锁的获取 :
- 线程第一次获取锁对象时,JVM将锁对象的Mark Word中偏向锁标志设为1 ,锁标志位设为01 ,同时将当前线程的ID存入Mark Word;
- 该线程后续再次获取锁时,只需判断Mark Word中的线程ID是否为当前线程ID :
✅ 是:直接获取锁,无需任何额外操作;
❌ 否:触发偏向锁的撤销或升级。
- 锁的释放 :偏向锁无需主动释放 ,只有当有其他线程竞争锁 时,才会触发偏向锁的撤销。
- 偏向锁的撤销 :
- 当其他线程尝试获取偏向锁时,JVM会先暂停持有锁的线程;
- 检查持有锁的线程是否还在执行:
✅ 是:将偏向锁升级为轻量级锁 ,继续执行;
❌ 否:将Mark Word恢复为无锁状态,重新偏向新线程。
核心优势
无需任何CAS操作(轻量级锁的核心),单线程下性能接近无锁,是JDK1.6后synchronized的默认锁状态 (JDK1.8默认开启,可通过-XX:-UseBiasedLocking关闭)。
2. 轻量级锁:轻度竞争场景的CAS实现
适用场景 :多个线程交替获取同一把锁 (无同时竞争),即轻度竞争场景,是偏向锁无法满足后的次优解。
核心设计 :基于CAS(Compare And Swap,比较并交换) 实现锁的获取与释放,无需进入内核态,避免线程阻塞的开销。
实现原理
前提 :轻量级锁的获取前,会先将锁对象的Mark Word复制到当前线程的栈帧中 ,创建一个锁记录(Lock Record) 对象,存储该副本。
- 锁的获取(CAS) :
线程通过CAS操作,将锁对象的Mark Word替换为指向当前线程栈帧中Lock Record的指针 :- ✅ CAS成功:将锁标志位设为00,轻量级锁获取成功;
- ❌ CAS失败:判断当前锁对象的Mark Word是否已指向当前线程的Lock Record (可重入):
✅ 是:直接获取锁;
❌ 否:说明有其他线程同时竞争锁,轻量级锁升级为重量级锁。
- 锁的释放(CAS) :
线程通过CAS操作,将栈帧中Lock Record的Mark Word副本 写回锁对象的Mark Word:- ✅ CAS成功:锁释放,Mark Word恢复为无锁状态;
- ❌ CAS失败:说明锁已升级为重量级锁,进入重量级锁的释放流程。
核心优势
基于用户态的CAS实现,无需内核态切换,性能远高于重量级锁;适用于线程交替执行的轻度竞争场景。
3. 重量级锁:重度竞争场景的操作系统实现
适用场景 :多个线程同时竞争同一把锁 (重度竞争),是synchronized的最终锁状态。
核心设计 :依赖操作系统的互斥量(Mutex) 和ObjectMonitor对象 实现,线程竞争失败会进入阻塞状态 ,触发内核态与用户态的切换。
实现原理
- 锁的获取 :
- 当轻量级锁CAS失败时,JVM会将锁对象的Mark Word替换为指向堆中ObjectMonitor对象的指针,锁标志位设为10,升级为重量级锁;
- 竞争锁的线程会进入ObjectMonitor的**_EntryList队列并阻塞**(操作系统的park()方法),放弃CPU执行权;
- 当持有锁的线程释放锁后,会唤醒_EntryList中的一个线程,重新竞争锁。
- 锁的释放 :
- 持有锁的线程执行完同步代码后,会调用unpark() 方法唤醒_EntryList中的一个线程;
- 将ObjectMonitor的
_owner设为null,同时将锁对象的Mark Word恢复为无锁/偏向锁状态; - 被唤醒的线程重新竞争锁,若竞争失败则再次阻塞。
核心缺点
线程阻塞/唤醒会触发内核态与用户态的切换 ,该操作开销极大,因此重量级锁的性能最低;但能保证多线程同时竞争时的互斥性。
4. 锁状态升级完整流程(总结)
无锁状态 → 第一个线程获取锁 → 偏向锁(线程ID存入Mark Word)
↓(有其他线程竞争,偏向锁撤销)
轻量级锁(CAS替换Mark Word为Lock Record指针)
↓(多个线程同时竞争,CAS失败)
重量级锁(Mark Word指向ObjectMonitor,线程阻塞)
五、重量级锁的底层:操作系统的同步原语
synchronized的重量级锁最终依托操作系统的底层同步原语 实现,核心涉及互斥量(Mutex) 、线程的阻塞与唤醒。
1. 内核态与用户态
Java程序运行在用户态 ,而线程的阻塞、唤醒、调度属于操作系统内核的功能 ,需要切换到内核态 ;
内核态与用户态的切换 是重量级锁性能低的核心原因,因为该操作需要保存线程的上下文,开销极大。
2. 核心系统调用
- park():线程阻塞的核心系统调用,由操作系统实现,让线程放弃CPU执行权,进入阻塞状态;
- unpark():线程唤醒的核心系统调用,唤醒被park()阻塞的线程,使其重新参与CPU调度;
- Mutex:操作系统的互斥量,实现线程的互斥访问,是ObjectMonitor的底层实现。
六、synchronized的三大特性实现(底层支撑)
synchronized能保证原子性、可见性、有序性 ,其底层由JVM的监视器机制 和内存模型(JMM) 支撑:
- 原子性 :通过监视器的互斥性实现,同一时刻只有一个线程能持有监视器,保证同步代码块的执行不可分割;
- 可见性 :通过JMM的刷新策略 实现,线程释放锁时,会将工作内存中的修改刷新到主内存 ;线程获取锁时,会将主内存中的最新数据加载到工作内存;
- 有序性 :通过JMM的指令重排规则 实现,synchronized修饰的代码块会被JVM视为一个同步屏障 ,屏障前后的指令禁止重排,同时保证代码块内的指令按顺序执行。
七、核心面试考点总结
- synchronized的字节码实现 :同步代码块用
monitorenter/monitorexit,同步方法用ACC_SYNCHRONIZED访问标志,底层均依赖Monitor; - 锁对象的核心载体 :锁状态存储在对象头的Mark Word中,通过修改Mark Word的锁标志位和存储内容实现锁状态切换;
- 锁升级的过程:偏向锁(无竞争)→轻量级锁(CAS,轻度竞争)→重量级锁(ObjectMonitor,重度竞争),单向升级,基于竞争程度自适应;
- 偏向锁的核心:单线程下无需CAS,直接通过线程ID判断,性能最优;
- 轻量级锁的核心:基于用户态CAS实现,无需内核态切换;
- 重量级锁的核心:依托操作系统的Mutex和ObjectMonitor,线程阻塞会触发内核态/用户态切换,性能最低;
- 与Lock的底层差异:synchronized是JVM原生实现(底层C++),自动加锁释放;Lock是Java代码实现(AQS),手动加锁释放,底层同样依赖CAS和LockSupport(park/unpark)。
八、整体总结
synchronized的底层实现是JVM+操作系统 的协同结果,其核心演进是从纯重量级锁到自适应锁:
- 表层 :通过字节码指令(
monitorenter/monitorexit/ACC_SYNCHRONIZED)实现锁的获取与释放; - 中层 :通过对象头的Mark Word存储锁状态,实现锁的升级与管理;
- 底层:偏向锁/轻量级锁基于JVM的CAS实现(用户态),重量级锁基于操作系统的Mutex和ObjectMonitor实现(内核态)。
JDK1.6后的一系列优化(偏向锁、轻量级锁、锁粗化、锁消除),让synchronized的性能与ReentrantLock接近,成为Java并发中简单、安全、高效的互斥锁选择。