一、synchronized 是什么?
synchronized 是 Java 语言原生提供的内置同步关键字 ,属于可重入的独占悲观锁,由 HotSpot JVM 底层(C++)实现,是解决多线程并发安全问题最基础、最常用的方案。
它的核心特性:
- 独占性:同一时间,只有一个线程能持有锁并执行被修饰的代码;
- 可重入性:持有锁的线程可以再次进入同一把锁修饰的代码块,不会发生死锁;
- JVM 自动管理:加锁、解锁、异常释放锁均由虚拟机自动完成,无需手动编码,降低使用门槛;
- 绑定对象锁:锁的载体是 Java 对象(实例对象 / Class 对象),每个对象都关联一个监视器锁(Monitor)。
二、synchronized 有什么用?
多线程并发访问共享资源 (成员变量、静态变量、公共对象等)时,会出现线程安全问题 (原子性、可见性、有序性问题),synchronized 的核心作用就是保证并发场景下的线程安全,具体解决三大问题:
| 问题类型 | 定义 | synchronized 如何解决 |
|---|---|---|
| 原子性 | 一个操作不可分割,要么全部执行成功,要么全部不执行 | 加锁后只有单线程执行同步代码,杜绝并发交叉执行 |
| 可见性 | 一个线程修改共享变量后,其他线程能立即感知到最新值 | 解锁时会将工作内存数据刷新到主内存,加锁时从主内存重新读取数据 |
| 有序性 | 禁止指令重排序,保证代码执行顺序符合编写逻辑 | 基于内存屏障机制,禁止锁内代码重排,保证执行有序 |
典型应用场景:
- 电商场景:库存扣减、订单创建,避免超卖、重复下单;
- 计数场景:多线程统计访问量、点赞数,避免数值计算错误;
- 共享对象操作:多线程修改集合、公共配置对象,保证数据一致性。
三、synchronized 的三种使用方式
根据锁的目标对象不同,分为三种用法,锁的范围和作用对象完全不同,这是开发中最容易出错的点:
1. 修饰实例方法
锁对象:当前实例对象(this),不同实例对象拥有独立的锁,互不干扰。
java
public class Stock {
// 共享资源:库存
private int count = 100;
// 修饰实例方法,锁当前this对象
public synchronized void deductStock() {
if (count > 0) {
count--;
System.out.println("库存剩余:" + count);
}
}
}
2. 修饰静态方法
锁对象:当前类的 Class 对象,全局唯一,所有实例共享同一把锁,同步范围最大。
java
public class Order {
// 静态共享资源
private static int orderNum = 10000;
// 修饰静态方法,锁 Order.class 对象
public static synchronized void createOrder() {
orderNum++;
System.out.println("生成订单号:" + orderNum);
}
}
3. 修饰同步代码块(推荐)
锁对象:自定义指定对象 (推荐使用 final 修饰的锁对象),可以灵活控制锁的粒度,性能最优。
java
public class Demo {
// 自定义锁对象,final 防止对象引用被修改导致锁失效
private final Object lock = new Object();
private int num = 0;
public void addNum() {
// 同步代码块,仅锁定核心逻辑,缩小锁范围
synchronized (lock) {
num++;
}
}
}
- 达到阈值(JVM 自适应调整)仍未获取锁;
- 出现第三个线程竞争锁(自旋会造成 CPU 空耗,触发升级)。
四、锁升级全流程与 synchronized 的底层实现
Java 对象头是锁升级的核心载体,对象头中的Mark Word字段会存储锁状态标识、线程 ID、锁指针等关键信息,不同锁状态对应不同的 Mark Word 结构。整个升级流程分为四个阶段,对应 synchronized 的四种工作模式:
阶段 1:无锁状态
- 场景:没有任何线程竞争同步资源
- 特点:对象头 Mark Word 标记为无锁,线程可以自由访问资源,无需加锁
- 这是 synchronized 加锁前的初始状态
阶段 2:偏向锁(Biased Lock)
适用场景
只有单个线程反复访问同步代码块,无多线程竞争,是锁升级的第一个状态。
工作原理
- 当第一个线程访问 synchronized 代码块时,JVM 通过 CAS 操作将线程 ID写入对象头的 Mark Word 中,同时将锁标记改为偏向锁;
- 该线程后续再次进入同步块时,只需比对 Mark Word 中的线程 ID,匹配成功则直接执行,无需 CAS 操作和线程阻塞;
- 偏向锁的核心优化:消除无竞争场景下的加锁 / 解锁开销。
升级触发条件
当第二个线程尝试竞争该锁时,偏向锁立即失效,升级为轻量级锁。
阶段 3:轻量级锁(Lightweight Lock)
适用场景
多线程交替执行同步代码块,短时间内无激烈的锁竞争(自旋即可获取锁)。
工作原理
- 竞争发生时,JVM 会在当前线程的栈帧中创建锁记录(Lock Record),将对象头的 Mark Word 拷贝到锁记录中;
- 线程通过CAS 操作尝试将对象头的 Mark Word 指向自己的锁记录,若成功则获取轻量级锁,执行同步代码;
- 若 CAS 失败,线程不会立即阻塞,而是执行自旋锁(循环尝试 CAS 获取锁),避免频繁切换内核态。
升级触发条件
- 自旋次数达到阈值(JVM 自适应调整)仍未获取锁;
- 出现第三个线程竞争锁(自旋会造成 CPU 空耗,触发升级)。
阶段 4:重量级锁(Heavyweight Lock)
适用场景
多线程高并发竞争,同步代码块执行时间较长,自旋会造成大量 CPU 资源浪费。
工作原理
- 锁升级为重量级锁后,JVM 会向操作系统申请互斥量(Mutex);
- 未获取锁的线程会被阻塞挂起,进入操作系统的等待队列,释放 CPU 资源;
- 持有锁的线程释放锁后,操作系统会唤醒等待队列中的线程,重新竞争锁。
特点
依赖操作系统内核实现,线程阻塞 / 唤醒会触发用户态↔内核态切换,性能开销最大,但能避免 CPU 空耗。
五、synchronized 与锁升级的核心关系
1. 从属与支撑关系
- 锁升级是 synchronized 的性能优化方案:没有锁升级机制,synchronized 始终是低效的重量级锁;锁升级让 synchronized 适配了不同并发场景,成为高效的内置锁。
- synchronized 是锁升级的应用载体:锁升级机制仅针对 Java 内置的 synchronized 锁生效,ReentrantLock 等显式锁不遵循该升级逻辑。
2. 动态适配关系
synchronized 的性能和行为完全由锁升级状态决定:
| 锁状态 | synchronized 性能 | 适用并发场景 | 核心开销 |
|---|---|---|---|
| 偏向锁 | 最高 | 单线程重复访问 | 几乎无开销 |
| 轻量级锁 | 较高 | 线程交替竞争、短时间同步 | CAS 操作 + 自旋开销 |
| 重量级锁 | 最低 | 高并发激烈竞争 | 内核态切换 + 线程阻塞开销 |
3. 底层绑定关系
锁升级的状态切换,直接修改对象头 Mark Word的结构,而 synchronized 加锁的本质,就是操作对象头的锁标记和关联数据,二者通过对象头完成底层绑定。
六.synchronized的重量级锁底层如何实现的?
前文一直提到了一个很重要的东西,叫**对象头 Mark Word,**这里就带大家认识一下
Synchronized中的重量级锁,底层就是基于**锁监视器(Monitor)**来实现的。简单来说就是锁对象头会指向一个锁监视器,而在监视器中则会记录一些信息,比如:
-
_owner:持有锁的线程
-
_recursions:锁重入次数
因此每一个锁对象,都会指向一个锁监视器,而每一个锁监视器,同一时刻只能被一个线程持有 ,这样就实现了互斥效果。但前提是,多个线程使用的是同一把锁。
前置核心概念
- 触发时机 :当轻量级锁的自旋获取锁失败、自旋次数超限,或出现多线程同时竞争时,JVM 会将锁升级为重量级锁;
- 核心载体 :HotSpot 中,每个 Java 对象关联一个
ObjectMonitor结构体(C++ 实现),这是重量级锁的核心数据结构; - 底层依赖 :重量级锁会脱离纯用户态实现,调用操作系统内核的互斥锁、线程调度接口,触发用户态与内核态的切换。
核心数据结构:ObjectMonitor
重量级锁的所有逻辑都围绕ObjectMonitor展开,它存储了锁的持有状态、等待线程队列、竞争线程队列等关键信息,核心关键字段如下:
cpp
// HotSpot源码简化版 ObjectMonitor 结构
class ObjectMonitor {
public:
// 标识当前持有锁的线程,NULL表示锁未被占用
void* _owner;
// 锁重入计数(支持synchronized可重入特性)
int _recursions;
// 竞争锁失败,进入阻塞状态的线程队列(双向链表)
ObjectWaiter* _EntryList;
// 调用了wait()方法,处于等待唤醒的线程队列
ObjectWaiter* _WaitSet;
// 锁的状态标记
int _count;
// 底层操作系统互斥量(实现内核级阻塞的核心)
pthread_mutex_t _mutex;
};
关键字段作用:
_owner:标记当前独占锁的线程,实现互斥访问;_recursions:记录重入次数,退出同步块时递减,为 0 才真正释放锁;_EntryList:存储尝试获取锁但失败的阻塞线程;_WaitSet:存储执行了wait()、需要被notify()唤醒的线程。
重量级锁底层完整执行流程
整个流程分为加锁、锁重入、线程等待 / 唤醒、释放锁 四个核心阶段,全程由 JVM 底层操作ObjectMonitor和操作系统内核完成:
阶段 1:锁升级与初始化
- 轻量级锁竞争失败后,JVM 会为目标对象分配并关联 ObjectMonitor;
- 将对象头
Mark Word的锁标记修改为重量级锁状态 ,并存储指向ObjectMonitor的指针; - 后续所有线程的加锁操作,都会通过这个监视器对象执行。
阶段 2:线程竞争加锁逻辑
- 线程尝试获取重量级锁时,首先通过操作系统互斥量
_mutex抢占锁权限; - 若
_owner为NULL(锁空闲):- 线程将
_owner指向自身,_recursions置为 1,成功获取锁,进入同步代码块执行;
- 线程将
- 若
_owner已指向其他线程(锁被占用):- 该线程被封装为
ObjectWaiter节点,加入_EntryList阻塞队列; - JVM 调用操作系统内核接口,将线程挂起阻塞 ,释放 CPU 资源,线程状态变为
BLOCKED;
- 该线程被封装为
- 阻塞期间线程完全不消耗 CPU,直到被唤醒后重新竞争锁。
阶段 3:锁重入实现
synchronized是可重入锁,底层通过_recursions字段实现:
- 持有锁的线程再次进入同一同步块 时,JVM 检测
_owner为当前线程,直接将_recursions + 1,无需重新竞争; - 每退出一层同步块,
_recursions - 1,直到计数归 0,才会真正释放锁。
阶段 4:wait () /notify () 唤醒逻辑
这是重量级锁独有的等待 / 唤醒机制,依赖_WaitSet队列:
- 持有锁的线程调用
object.wait():- 释放当前持有的锁(重置
_owner、_recursions); - 线程从
_EntryList移除,加入_WaitSet等待队列,进入WAITING状态;
- 释放当前持有的锁(重置
- 其他线程调用
object.notify()/notifyAll():- JVM 从
_WaitSet中取出线程节点,转移回_EntryList; - 线程被唤醒,重新参与锁竞争。
- JVM 从
阶段 5:释放锁与线程唤醒
- 持有锁的线程执行完同步代码块,JVM 开始释放锁:
_recursions递减,若计数为 0,将_owner置为NULL,释放操作系统互斥量_mutex;
- 监视器对象唤醒
_EntryList中的阻塞线程(唤醒策略由 JVM 实现,如公平 / 非公平); - 被唤醒的线程重新竞争锁,竞争成功则占用锁,失败则重新回到阻塞队列。
底层关键技术细节
1. 用户态与内核态切换
重量级锁最大的性能开销来源:
- 线程阻塞、唤醒、互斥量操作,都需要调用操作系统内核 API,触发用户态→内核态→用户态的切换;
- 切换过程涉及上下文保存 / 恢复、内核权限校验,开销远大于用户态的 CAS 操作。
2. 与轻量级锁的核心区别
| 特性 | 轻量级锁 | 重量级锁 |
|---|---|---|
| 实现层级 | JVM 用户态纯代码实现 | 依赖操作系统内核实现 |
| 线程等待方式 | 自旋循环(消耗 CPU) | 内核挂起阻塞(不消耗 CPU) |
| 核心数据结构 | 线程栈帧中的锁记录 | ObjectMonitor 监视器 |
| 性能开销 | 低(无状态切换) | 高(存在内核态切换) |
| 适用场景 | 短时间、低竞争 | 长时间、高竞争 |
3. 非公平特性
synchronized重量级锁默认是非公平锁:被唤醒的阻塞线程,与新创建的线程会同时竞争锁,不保证等待时间最长的线程优先获取锁,这能提升吞吐量,但可能造成线程饥饿。
重量级锁的优缺点
优点
- 高竞争下更高效:线程阻塞后释放 CPU,避免自旋导致的 CPU 空转浪费,适合同步代码执行耗时较长的场景;
- 功能完整 :支持
wait()/notify()等待唤醒机制,支持锁重入、互斥访问,满足复杂并发场景; - 稳定性强:依赖操作系统原生调度,不会因自旋耗尽系统资源。
缺点
- 性能开销大:线程阻塞 / 唤醒的内核态切换成本高,无竞争 / 低竞争场景下远不如偏向锁、轻量级锁;
- 灵活性低:无法手动控制超时、中断、公平性,仅支持 JVM 默认策略。
补充:与 ReentrantLock 底层的差异
很多人会对比synchronized重量级锁和ReentrantLock,二者核心区别:
synchronized重量级锁:JVM 底层 C++ 实现,绑定 ObjectMonitor,依赖操作系统互斥量;ReentrantLock:Java 代码层基于 AQS 实现,使用 CAS + 双向队列 + LockSupport,不依赖 ObjectMonitor,也没有内核态的强绑定。
总结
- 核心实现 :
synchronized重量级锁以 HotSpot 的ObjectMonitor(C++ 结构体)为核心,依赖操作系统互斥量实现线程互斥; - 执行逻辑 :锁被占用时,竞争线程进入
_EntryList阻塞队列并被内核挂起,锁释放后唤醒线程重新竞争,同时通过_recursions支持可重入; - 关键开销 :线程调度依赖用户态与内核态切换,这是重量级锁性能较低的核心原因;
- 适用场景 :专为高并发、长时间占用锁的场景设计,用线程阻塞换取 CPU 资源的合理利用。
