JMM 进阶:彻底理解 synchronized 实现原理

目录

[一、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 (锁对象) {
    // 临界区代码
}

可以自由指定锁:thisclass、自定义对象等

实际开发优先用代码块,粒度更小,性能更好

五、底层原理

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 默认开启,优化单线程场景)

场景 :只有一个线程反复获取锁,无竞争

  1. 线程第一次进入同步代码块:
    • CAS 将 Mark Word 中偏向线程 ID改为当前线程 ID,偏向锁位 = 1,锁标志 = 01
  2. 后续同一线程再获取:直接判断线程 ID,无 CAS,零开销
  3. 偏向锁撤销 :当其他线程尝试抢锁 ,触发撤销:
    • 暂停持有偏向锁的线程
    • 清空偏向线程 ID,升级为轻量级锁

适用:单线程频繁加锁,消除无意义 CAS 开销

2. 轻量级锁(自旋锁,低并发竞争)

场景 :多线程交替执行,竞争不激烈,线程短暂等待

  1. 线程创建 栈帧中的锁记录(Lock Record),复制对象 Mark Word 到锁记录

  2. 通过 CAS 尝试把对象 Mark Word 指向当前线程的锁记录:

    • CAS 成功 :获取轻量级锁,锁标志改为 00

    • CAS 失败 :说明有线程已持有锁,当前线程自旋等待(循环重试 CAS)

  3. 解锁:CAS 把锁记录数据写回对象头,释放锁

自旋弊端 :自旋会占用 CPU,若自旋次数过多仍抢不到锁,直接升级重量级锁

3. 重量级锁(依赖 OS 内核,高并发竞争)

场景 :并发激烈、自旋失败、多线程同时抢锁。底层依赖 ObjectMonitor(监视器锁),由操作系统实现

ObjectMonitor 核心结构

_owner:当前持有锁的线程

_WaitSet:等待集合(wait() 线程)

_EntryList:阻塞队列(抢锁失败的线程)

_count:锁重入次数

执行流程
  1. 线程抢锁失败 → 进入 EntryList 阻塞队列,线程挂起(BLOCKED),让出 CPU

  2. 持有锁的线程释放锁后,唤醒 EntryList 中的线程重新竞争

  3. 调用 wait():线程进入 WaitSet 等待,需 notify()/notifyAll() 唤醒,再重新进入竞争队列

重量级锁会触发用户态 ↔ 内核态切换,开销最大

3、对象头 Mark WordObjectMonitor关系

对象头 Mark Word 存储指向 ObjectMonitor 的指针,二者是「引用与实例」的关系,仅重量级锁阶段才会关联

1. 无锁 / 偏向锁 / 轻量级锁

这三种状态下,完全不涉及 ObjectMonitor

  • Mark Word 只存:线程 ID、锁记录指针、哈希码、分代年龄等信息
  • 线程靠 CAS、栈中 Lock Record 实现互斥,全程在用户态,不创建、不访问 ObjectMonitor

2. 重量级锁(核心关联阶段)

当锁膨胀为重量级锁后:

  1. JVM 在堆中创建 ObjectMonitor 监视器对象

  2. 对象头 Mark Word 改写 :把原本的锁信息,替换成 ObjectMonitor 的内存地址指针 (锁标志位变为 10

  3. 后续所有抢锁线程,都会通过对象头里的这个指针,找到对应的 ObjectMonitor 实例

简单对应:

Java 对象 → 对象头 (Mark Word) → 指针 → 堆里的 ObjectMonitor

3. 对象头(Mark Word)

作用:锁状态标识 + 寻址入口

  • 记录当前锁处于哪种状态(无锁 / 偏向 / 轻量 / 重量)。
  • 重量级锁时,唯一作用就是保存 ObjectMonitor* 指针,让线程能定位到监视器。
  • 存储空间极小,复用位域,不存队列、计数、等待线程等复杂数据。

4. ObjectMonitor

**作用:锁的核心逻辑载体(真正的 "锁本体")**是 C++ 实现的结构体,持有所有同步管理数据:

  • _owner:当前持有锁的线程
  • _count:重入次数
  • _EntryList:抢锁失败的阻塞线程队列
  • _WaitSet:调用 wait() 进入等待的线程队列
  • 负责线程阻塞、唤醒、等待 / 通知、重入等全部逻辑。

5.关键执行流程(重量级锁场景)

  1. 线程访问 synchronized 代码块,发现对象头锁标志为 10(重量级锁)
  2. 从 Mark Word 取出 ObjectMonitor 指针
  3. 访问 ObjectMonitor
    • _owner 为空:当前线程占有锁,_owner 设为自身,_count=1
    • _owner 是当前线程:重入,_count++
    • 若被其他线程持有:当前线程进入 _EntryList,操作系统挂起线程
  4. 解锁时:_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
相关推荐
小白不白1111 小时前
Invoke的用法
开发语言·人工智能·数码相机·计算机视觉·c#
techdashen1 小时前
What is maintenance, anyway?
开发语言·后端·rust
戳代码的新星1 小时前
论小白如何学会使用Maven
java·maven
wyhwust1 小时前
maven的安装和配置
java
万法若空1 小时前
C/C++基本类型表示范围
c语言·开发语言·c++
yijianace1 小时前
Python爬虫实战:BooksToScrape 多线程爬取与图片下载
开发语言·爬虫·python
plainGeekDev1 小时前
HttpURLConnection → OkHttp + Kotlin
android·java·kotlin
凡人叶枫1 小时前
Effective C++ 条款15:在资源管理类中提供对原始资源的访问
linux·开发语言·c++·stm32·单片机
swordbob1 小时前
Spring Boot 2.0 改 CGLIB 后,接口实现是否有影响
java·开发语言·spring