目录
[一、synchronized 是什么](#一、synchronized 是什么)
[二、为什么需要 synchronized](#二、为什么需要 synchronized)
[三、synchronized 的三种用法](#三、synchronized 的三种用法)
[六、synchronized 如何保证三大特性](#六、synchronized 如何保证三大特性)
[七、synchronized 的可重入性](#七、synchronized 的可重入性)
[八、synchronized 和 volatile 区别](#八、synchronized 和 volatile 区别)
[九、synchronized 和 CAS 区别](#九、synchronized 和 CAS 区别)
synchronized 是 Java 并发编程中最基础、最重要的线程同步机制之一
-
volatile解决的是 可见性(可看前两篇文章) -
CAS解决的是 原子性(乐观锁)(可看前两篇文章) -
synchronized解决的是 原子性 + 可见性 + 有序性
一、synchronized 是什么
synchronized 不是 Java 代码写出来的类,也不是普通方法。是 JVM 内置锁(Monitor Lock)
Java 虚拟机(JVM)内置的、基于对象头实现的互斥锁机制
换句话说:
synchronized是关键字- 真正实现加锁、解锁的,是 JVM 底层的 C++ 代码
- Java 层面看不到源码,只能看到 JVM 指令
作用:
同一时刻只允许一个线程进入临界区执行代码
例如:
java
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
当多个线程同时调用:
java
counter.increment();
只有一个线程能够进入方法,其它线程必须等待
二、为什么需要 synchronized
多线程环境下,如果不加锁,会出现:
-
原子性问题:一步操作被多个线程打断
-
可见性问题:一个线程改了值,另一个线程看不到
-
有序性问题:指令重排导致逻辑错乱
Demo:
java
public class Demo {
private static int count = 0;
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
count++;
}
}).start();
}
Thread.sleep(3000);
System.out.println(count);
}
}
理论结果是100000
实际结果:86432、92311、97121...(并发安全问)
因为 count++; 不是原子操作
实际执行:
读取 count
count + 1
写回 count
多个线程同时执行会发生覆盖。
使用 synchronized结果稳定为100000
java
public synchronized void add() {
count++;
}
三、synchronized 的三种用法
1、修饰实例方法
java
public synchronized void test() {
}
锁对象:
this
等价于:
java
public void test() {
synchronized(this) {
}
}
2、修饰静态方法
java
public static synchronized void test() {
}
锁对象:
当前Class对象:Demo.class
java
synchronized(Demo.class) {
}
3、修饰代码块
java
synchronized(lock){
}
例如:
java
private final Object lock = new Object();
public void add() {
synchronized(lock) {
count++;
}
}
四、锁的到底是什么
1、修饰实例方法
java
public synchronized void method() {
// 同一时间只有一个线程能进这里
}
锁的是 调用这个方法的对象, 同一个对象的多个 synchronized 方法会互斥
两个线程调用 同一个对象
线程1 → a1.method()
线程2 → a1.method()
互斥!必须排队! 因为锁的是 同一个 a1
两个线程调用 不同对象
线程1 → a1.method()
线程2 → a2.method()
不互斥!同时运行! 因为锁的是 两个不同对象,各锁各的,互不影响
2、修饰静态方法(锁 类对象 Class)
java
public static synchronized void staticMethod() {
// 锁的是类,全局唯一
}
锁的是 类的 Class 对象 ,属于全局锁,所有实例共用一把锁
java
public class User {
// 锁的是:User.class 这个全局唯一对象
public static synchronized void test() {
}
}
等于:
java
synchronized (User.class) {
}
不管你创建多少个对象:
java
User u1 = new User();
User u2 = new User();
User u3 = new User();
只要调用静态同步方法:
java
u1.test();
u2.test();
User.test();
全部都抢同一把锁:User.class
3、修饰代码块(锁 指定对象,灵活)
java
synchronized (锁对象) {
// 临界区代码
}
可以自由指定锁:this、class、自定义对象等
实际开发优先用代码块,粒度更小,性能更好
五、底层原理
1、Java 对象头(Mark Word)
锁信息全部存在对象头的 Mark Word 里(64 位 JDK):
-
普通对象头:
Mark Word(64bit) + 类型指针(64bit) -
Mark Word 结构(核心:锁标志位 + 偏向锁位 ):
锁状态 偏向锁位 锁标志位 无锁 0 01 偏向锁 1 01 轻量级锁 0 00 重量级锁 0 10 GC 标记 0 11
Mark Word 会动态复用空间,不同锁状态存储不同数据(线程 ID、锁记录指针、Monitor 指针等)
2、锁的四种状态
锁会随竞争逐步升级 :无锁 → 偏向锁 → 轻量级锁 → 重量级锁,只能升级,不能降级
1. 偏向锁(JDK6 默认开启,优化单线程场景)
场景 :只有一个线程反复获取锁,无竞争
- 线程第一次进入同步代码块:
- CAS 将 Mark Word 中偏向线程 ID改为当前线程 ID,偏向锁位 = 1,锁标志 = 01
- 后续同一线程再获取:直接判断线程 ID,无 CAS,零开销
- 偏向锁撤销 :当其他线程尝试抢锁 ,触发撤销:
- 暂停持有偏向锁的线程
- 清空偏向线程 ID,升级为轻量级锁
适用:单线程频繁加锁,消除无意义 CAS 开销
2. 轻量级锁(自旋锁,低并发竞争)
场景 :多线程交替执行,竞争不激烈,线程短暂等待
-
线程创建 栈帧中的锁记录(Lock Record),复制对象 Mark Word 到锁记录
-
通过 CAS 尝试把对象 Mark Word 指向当前线程的锁记录:
-
CAS 成功 :获取轻量级锁,锁标志改为
00 -
CAS 失败 :说明有线程已持有锁,当前线程自旋等待(循环重试 CAS)
-
-
解锁:CAS 把锁记录数据写回对象头,释放锁
自旋弊端 :自旋会占用 CPU,若自旋次数过多仍抢不到锁,直接升级重量级锁
3. 重量级锁(依赖 OS 内核,高并发竞争)
场景 :并发激烈、自旋失败、多线程同时抢锁。底层依赖 ObjectMonitor(监视器锁),由操作系统实现
ObjectMonitor 核心结构
_owner:当前持有锁的线程
_WaitSet:等待集合(wait() 线程)
_EntryList:阻塞队列(抢锁失败的线程)
_count:锁重入次数
执行流程
-
线程抢锁失败 → 进入
EntryList阻塞队列,线程挂起(BLOCKED),让出 CPU -
持有锁的线程释放锁后,唤醒
EntryList中的线程重新竞争 -
调用
wait():线程进入WaitSet等待,需notify()/notifyAll()唤醒,再重新进入竞争队列
重量级锁会触发用户态 ↔ 内核态切换,开销最大
3、对象头 Mark Word 与ObjectMonitor关系

对象头 Mark Word 存储指向 ObjectMonitor 的指针,二者是「引用与实例」的关系,仅重量级锁阶段才会关联
1. 无锁 / 偏向锁 / 轻量级锁
这三种状态下,完全不涉及 ObjectMonitor
- Mark Word 只存:线程 ID、锁记录指针、哈希码、分代年龄等信息
- 线程靠 CAS、栈中 Lock Record 实现互斥,全程在用户态,不创建、不访问 ObjectMonitor
2. 重量级锁(核心关联阶段)
当锁膨胀为重量级锁后:
-
JVM 在堆中创建 ObjectMonitor 监视器对象
-
对象头 Mark Word 改写 :把原本的锁信息,替换成 ObjectMonitor 的内存地址指针 (锁标志位变为
10) -
后续所有抢锁线程,都会通过对象头里的这个指针,找到对应的 ObjectMonitor 实例
简单对应:
Java 对象 → 对象头 (Mark Word) → 指针 → 堆里的 ObjectMonitor
3. 对象头(Mark Word)
作用:锁状态标识 + 寻址入口
- 记录当前锁处于哪种状态(无锁 / 偏向 / 轻量 / 重量)。
- 重量级锁时,唯一作用就是保存
ObjectMonitor*指针,让线程能定位到监视器。 - 存储空间极小,复用位域,不存队列、计数、等待线程等复杂数据。
4. ObjectMonitor
**作用:锁的核心逻辑载体(真正的 "锁本体")**是 C++ 实现的结构体,持有所有同步管理数据:
_owner:当前持有锁的线程_count:重入次数_EntryList:抢锁失败的阻塞线程队列_WaitSet:调用wait()进入等待的线程队列- 负责线程阻塞、唤醒、等待 / 通知、重入等全部逻辑。
5.关键执行流程(重量级锁场景)
- 线程访问
synchronized代码块,发现对象头锁标志为10(重量级锁) - 从 Mark Word 取出 ObjectMonitor 指针
- 访问
ObjectMonitor:- 若
_owner为空:当前线程占有锁,_owner设为自身,_count=1 - 若
_owner是当前线程:重入,_count++ - 若被其他线程持有:当前线程进入
_EntryList,操作系统挂起线程
- 若
- 解锁时:
_count--,归 0 后清空_owner,唤醒队列里的线程
六、synchronized 如何保证三大特性
1 原子性
例如:
java
synchronized(lock){
count++;
}
执行期间:
读取
修改
写回
其它线程无法进入,因此具有原子性
2 可见性
线程释放锁时:
unlock
JMM规定:必须把工作内存刷新到主内存
线程A
count=10
↓
释放锁
↓
刷新主内存
线程B获取锁:重新读取主内存,因此保证可见性
3 有序性
JMM规定:
unlock
happens-before
lock
java
synchronized(lock){
}
内部代码不会被随意重排序到锁外。
七、synchronized 的可重入性
锁重入同一线程多次获取同一把锁:
- 偏向锁 / 轻量级锁:判断线程 ID 一致,直接放行。
- 重量级锁:
ObjectMonitor._count计数 +1,解锁时递减,归 0 才真正释放。
例如:
java
public synchronized void methodA() {
methodB();
}
public synchronized void methodB() {
}
如果不可重入:
methodA获得锁
↓
调用methodB
↓
再次申请锁
↓
死锁
八、synchronized 和 volatile 区别
| 对比项 | synchronized | volatile |
|---|---|---|
| 原子性 | √ | × |
| 可见性 | √ | √ |
| 有序性 | √ | √ |
| 阻塞线程 | √ | × |
| 底层 | Monitor | 内存屏障 |
| 适用场景 | 复合操作 | 状态标记 |
例如:
java
volatile boolean stop = false;
适合做标志位:
java
while (!stop) {
}
但是非原子操作仍然线程不安全:
java
volatile int count;
count++;
九、synchronized 和 CAS 区别
| 对比 | synchronized | CAS |
|---|---|---|
| 思想 | 悲观锁 | 乐观锁 |
| 是否阻塞 | 是 | 否 |
| CPU消耗 | 低 | 高(自旋) |
| 竞争激烈 | 更稳定 | 失败率高 |
| 实现 | Monitor | Unsafe/VarHandle |