Java Synchronized底层原理:Monitor机制、锁膨胀、自旋优化与偏向锁细节解密
深入剖析Java Synchronized底层原理:从对象头到锁升级的全面解读
📖 摘要
关键词:
Java并发
、Synchronized原理
、锁升级机制
、对象头
、Monitor
本文将深入解析Java中synchronized
关键字的底层实现,涵盖对象头结构、Monitor机制、锁升级流程等核心内容,帮助开发者理解高并发场景下的锁优化策略。无论是面试准备还是性能调优,本文都能为你提供清晰的技术洞察!
📑 目录
- 引言:为什么需要了解synchronized底层原理?
- 对象头与Mark Word的奥秘
- Monitor(管程)机制详解
- 锁升级:偏向锁→轻量级锁→重量级锁
- synchronized的可重入性实现
- 总结与面试题
1. 引言:为什么需要了解synchronized底层原理?
在多线程编程中,synchronized
是Java开发者最常用的同步工具。但你是否遇到过这些问题?
- 性能瓶颈:简单粗暴地加锁导致吞吐量下降?
- 死锁问题:线程互相等待却不知如何排查?
- 面试必问:如何回答“synchronized和ReentrantLock的区别”?
理解其底层原理,不仅能优化高并发程序,还能深入JVM设计思想,更是大厂面试的核心考点!
2. 对象头与Mark Word的奥秘
每个Java对象都暗藏玄机!对象内存布局分为三部分:
其中Mark Word是实现锁的关键,其结构随锁状态动态变化(以64位JVM为例):
锁状态 | 存储内容 | 标识位 |
---|---|---|
无锁 | unused(25bit)、hashcode(31bit)、分代年龄(4bit)等 | 001 |
偏向锁 | 线程ID(54bit)、Epoch(2bit)、分代年龄(4bit) | 101 |
轻量级锁 | 指向栈中锁记录的指针(62bit) | 00 |
重量级锁 | 指向Monitor的指针(62bit) | 10 |
3. Monitor(管程)机制详解
当使用synchronized
时,JVM会通过**Monitor(管程)**实现互斥,其核心结构如下:
class Monitor {
Thread* owner; // 持有锁的线程
EntryList* entrySet; // 阻塞等待锁的线程队列
WaitSet* waitSet; // 调用wait()后的等待队列
int recursions; // 重入次数计数器
};
执行流程:
- 线程通过
monitorenter
指令尝试获取Monitor所有权。 - 若Owner为空,则当前线程成为Owner,recursions=1。
- 若Owner是当前线程,recursions++(可重入性)。
- 竞争失败则进入EntryList阻塞等待。
Monitor的完整结构:线程如何被管理?
Monitor是synchronized
实现互斥的核心,其完整结构在JVM源码(如objectMonitor.hpp
)中定义如下:
class ObjectMonitor {
void* _header; // 对象头(存储Mark Word)
void* _owner; // 持有锁的线程指针
volatile intptr_t _recursions; // 重入次数
ObjectWaiter* _EntryList; // 竞争锁的线程队列(阻塞态)
ObjectWaiter* _WaitSet; // 调用wait()后的线程队列(等待态)
volatile int _WaitSetLock;// 保护WaitSet的锁
// ... 其他字段省略
};
关键队列说明:
-
EntryList:
当线程A持有锁时,线程B尝试获取锁失败,会被封装为ObjectWaiter
节点加入EntryList,并进入BLOCKED
状态。
唤醒规则:锁释放时,JVM会从EntryList中选择线程(非公平模式下可能直接唤醒最新竞争者)。 -
WaitSet:
当线程调用wait()
方法后,线程会释放锁并进入WaitSet,状态变为WAITING
。
唤醒条件:其他线程调用notify()/notifyAll()
后,线程从WaitSet转移到EntryList重新竞争锁。
2. 锁膨胀(Lock Inflation):轻量级锁如何升级为重量级锁?
触发条件:
- 轻量级锁CAS替换Mark Word失败(多线程竞争)。
- 自旋锁尝试超过阈值(JDK6后为自适应自旋)。
cas替换
替换成功
膨胀流程(源码级解析):
1. 线程A持有轻量级锁(Mark Word指向栈中Lock Record)
2. 线程B竞争锁,CAS失败 → 开始自旋
3. 自旋失败 → JVM准备膨胀为重量级锁
4. 创建ObjectMonitor对象,初始化Owner、EntryList等
5. 线程A释放轻量级锁时,发现锁已膨胀:
a. 修改Mark Word为指向ObjectMonitor的指针(锁标志位10)
b. 唤醒EntryList中的线程B进入锁竞争
6. 后续所有锁操作直接通过ObjectMonitor进行
重量级锁的核心代价:
- 上下文切换:线程竞争失败后从用户态陷入内核态,由操作系统调度阻塞与唤醒。
- 吞吐量下降:适合高竞争但线程执行时间长的场景(如数据库连接池争抢)。
3. 自旋优化(Spin Locking):竞争时的CPU空转策略
自旋锁的意义:
- 减少上下文切换:线程不立即阻塞,而是循环尝试获取锁(忙等待)。
- 适用场景:锁竞争时间短、线程数少(如单核CPU或低并发)。
JDK的自适应自旋优化(Adaptive Spinning):
- 动态调整:根据上次自旋成功次数动态决定本次自旋时间。
- 实现逻辑:
- 若上次自旋成功获得锁,则允许更长的自旋时间。
- 若自旋很少成功,则直接跳过自旋进入阻塞。
自旋示例代码(伪代码):
void EnterSpinLock() {
int spins = 0;
while (!tryLock() && spins < max_spins) {
++spins;
Thread::SpinWait(); // CPU空转(如执行PAUSE指令)
}
if (!tryLock()) {
EnterBlockingQueue(); // 加入EntryList并阻塞
}
}
4. 锁升级:偏向锁→轻量级锁→重量级锁
锁升级是JVM优化性能的关键策略!流程图解:
┌───────────┐
│ 无锁 │
└─────┬─────┘
│ 首次访问
┌─────▼─────┐
│ 偏向锁 │◄───┐
└─────┬─────┘ │同一线程多次访问
│ 竞争发生 │
┌─────▼─────┐ │
│ 轻量级锁 │───►┘
└─────┬─────┘
│ CAS失败
┌─────▼─────┐
│ 重量级锁 │
└───────────┘
4.1 偏向锁(Biased Lock)
- 场景:单线程重复访问同步块。
- 原理:Mark Word中记录线程ID,后续无需CAS。
- 示例:Web服务中用户独享资源的处理。
4.2 轻量级锁(Lightweight Lock)
- 场景:低竞争的多线程环境。
- 原理:
- 线程栈中创建Lock Record,拷贝Mark Word。
- 通过CAS将对象头指向Lock Record。
- 失败:触发自旋锁(JDK6前默认10次,后改为自适应)。
4.3 重量级锁(Heavyweight Lock)
- 场景:高并发激烈竞争。
- 原理:依赖OS的Mutex Lock,线程进入阻塞状态。
- 代价:用户态到内核态的上下文切换。
JDK 15后偏向锁的默认禁用原因:
- 维护成本高:撤销操作需STW(Stop-The-World),影响GC暂停时间。
- 收益下降:现代应用多线程竞争普遍,偏向锁适用场景减少。
- 禁用方式:通过JVM参数
-XX:-UseBiasedLocking
关闭。
5. synchronized的可重入性实现
通过Monitor的recursions计数器实现:
public class ReentrantDemo {
public synchronized void methodA() {
methodB(); // 可重入
}
public synchronized void methodB() {
// 计数器递增至2
}
}
- 每个锁操作对应
recursions++
,退出时recursions--
。 - 当
recursions=0
时,锁完全释放。
6. 总结与面试题
核心总结表:
机制 | 优势 | 劣势 | 适用场景 |
---|---|---|---|
偏向锁 | 单线程零成本加锁 | 撤销开销大 | 明确单线程访问 |
轻量级锁 | CAS无阻塞竞争 | 自旋消耗CPU | 低竞争、短临界区 |
重量级锁 | 高竞争下稳定 | 上下文切换开销 | 高并发、长临界区 |
1. 锁膨胀是可逆的吗?
不可逆。一旦升级为重量级锁,无法降级,但偏向锁可重置为无锁。
2. 为什么wait()必须在synchronized块中调用?
wait()会释放锁,需先获取Monitor的所有权(否则抛出IllegalMonitorStateException)。
3. 自旋锁是否一定优于阻塞?
4.synchronized vs ReentrantLock对比
特性 | synchronized | ReentrantLock |
---|---|---|
锁实现 | JVM内置,自动释放 | API层面,需手动unlock() |
中断响应 | 不支持 | 支持lockInterruptibly() |
公平锁 | 非公平 | 可配置公平策略 |
条件队列 | 单一wait/notify | 支持多个Condition |
性能 | JDK6后优化后接近 | 高竞争场景更灵活 |