synchronized 的底层原理及优化机制

synchronized 是 Java 内置的悲观锁 ,核心作用是保证多线程环境下的原子性、可见性和有序性,从 JDK 1.6 开始进行了大量优化(如偏向锁、轻量级锁),性能大幅提升,成为并发编程中最常用的同步手段之一。

一、底层核心原理

synchronized 的实现依赖 Java 对象头(Mark Word)监视器锁(Monitor) ,其核心逻辑是:通过竞争 Monitor 的所有权实现线程互斥

1. 核心依赖:监视器锁(Monitor)

Monitor 是 JVM 层面的抽象概念,本质是一种同步工具,可理解为 "锁的持有者管理机制",每个 Java 对象都隐式关联一个 Monitor(即 "对象锁" 的底层载体)。

Monitor 的结构(简化):

  • Owner:当前持有锁的线程(同一时间只能有一个线程持有)。
  • EntryList:等待获取锁的线程队列(线程处于 BLOCKED 状态)。
  • WaitSet :调用 wait() 后释放锁的线程队列(线程处于 WAITING 状态)。

Monitor 的核心逻辑:

  1. 线程尝试获取锁时,会竞争 Monitor 的 Owner 位置:

    • 若 Owner 为空,当前线程直接成为 Owner,持有锁。
    • 若 Owner 已被其他线程占用,当前线程进入 EntryList 阻塞。
  2. 线程释放锁时(退出同步块 / 方法、调用 wait()):

    • 若调用 wait(),线程释放 Owner 身份,进入 WaitSet 等待被唤醒。
    • 若正常释放,Owner 置空,JVM 从 EntryList 唤醒一个线程竞争锁。

2. 锁状态的存储基础:Java 对象头(Mark Word)

Java 对象在内存中分为 3 部分:对象头、实例数据、对齐填充 。其中,对象头的 Mark Word 是存储锁状态的关键

Mark Word 的结构(动态变化,32 位 JVM 示例):

锁状态 Mark Word 存储内容
无锁 HashCode(25 位) + 对象年龄(4 位) + 是否偏向锁(1 位,0) + 锁状态(2 位,01)
偏向锁 偏向线程 ID(23 位) + Epoch(2 位) + 对象年龄(4 位) + 是否偏向锁(1 位,1) + 锁状态(2 位,01)
轻量级锁 指向栈帧中 "锁记录(Lock Record)" 的指针(30 位) + 锁状态(2 位,00)
重量级锁 指向 Monitor 的指针(30 位) + 锁状态(2 位,11)
  • 锁状态由 2 位标志位 + 是否偏向锁标志位 共同决定。
  • Mark Word 的动态变化是 synchronized 锁升级的核心依据。

3. synchronized 的两种使用方式及底层实现

synchronized 可修饰方法代码块,底层实现略有差异,但核心都是竞争 Monitor。

(1)修饰代码块:monitorenter + monitorexit 指令

编译后,同步代码块的前后会插入 monitorentermonitorexit 字节码指令:

  • monitorenter:线程进入时执行,尝试获取 Monitor 所有权(成功则 Owner 设为当前线程,失败则阻塞)。
  • monitorexit:线程退出时执行,释放 Monitor 所有权(Owner 置空,唤醒等待线程)。

注意:编译器会生成 2 个 monitorexit:一个对应正常退出,一个对应异常退出(确保锁一定释放,避免死锁)。

(2)修饰方法:ACC_SYNCHRONIZED 标志

修饰方法时,字节码中不会插入指令,而是在方法表(method_info) 中添加 ACC_SYNCHRONIZED 标志:

  • 线程调用方法时,JVM 检查该标志:若存在,先获取 Monitor 锁,再执行方法体。
  • 方法执行完毕(正常返回 / 抛出异常),JVM 自动释放 Monitor 锁。

类锁 vs 对象锁

  • 对象锁 :修饰实例方法或代码块(锁对象为 this 或自定义对象),竞争的是 "实例对象关联的 Monitor"。
  • 类锁 :修饰静态方法或代码块(锁对象为 XXX.class),竞争的是 "类对象(Class 实例)关联的 Monitor"。
  • 本质:类锁也是对象锁(Class 是 JVM 加载的单例对象),两者独立,互不干扰。

二、JDK 1.6+ 核心优化机制

早期 synchronized 是 "重量级锁",线程竞争失败会直接阻塞(切换到内核态,开销大)。JDK 1.6 引入锁升级机制 和其他优化,让 synchronized 在无竞争 / 轻度竞争场景下性能接近乐观锁(如 ReentrantLock)。

核心优化思路:根据竞争强度动态切换锁状态(无锁 → 偏向锁 → 轻量级锁 → 重量级锁),减少无竞争 / 轻度竞争时的开销

1. 锁升级机制(核心优化)

锁升级是不可逆的(只能从低开销向高开销升级),目的是 "按需分配资源":无竞争时用偏向锁,轻度竞争时用轻量级锁,激烈竞争时用重量级锁。

(1)偏向锁:无竞争场景的最优解

适用场景 :锁由同一线程多次获取,无其他线程竞争(如单线程循环调用同步方法)。核心思想:"偏向" 第一个获取锁的线程,后续该线程无需竞争,直接持有锁。

实现原理:
  1. 线程第一次获取锁时,通过 CAS 将 Mark Word 中的 "偏向线程 ID" 设为当前线程 ID,"是否偏向锁" 设为 1,锁状态保持 01(偏向锁标识)。

  2. 后续该线程再次获取锁时,仅需检查:

    • Mark Word 中的偏向线程 ID 是否为当前线程。
    • 偏向锁标志是否为 1。
    • 锁状态是否为 01。满足则直接获取锁,无需 CAS 或阻塞,开销极低。
偏向锁的撤销:

当有其他线程尝试竞争偏向锁时,偏向锁会被 "撤销",升级为轻量级锁。撤销流程:

  1. 暂停持有偏向锁的线程(STW 短暂停顿)。

  2. 检查持有线程的状态:

    • 若线程已终止,直接将 Mark Word 重置为无锁状态。
    • 若线程仍存活,将锁升级为轻量级锁,持有线程继续执行,竞争线程进入轻量级锁竞争。

(2)轻量级锁:轻度竞争场景的优化

适用场景 :少量线程竞争锁,且竞争时间短(如两个线程交替获取锁)。核心思想:用 "自旋" 替代 "阻塞",避免线程切换到内核态的开销。

实现原理:
  1. 线程获取锁时,先在当前栈帧中创建一个 Lock Record(锁记录) ,存储对象当前的 Mark Word 副本(Displaced Mark Word)。

  2. 通过 CAS 将对象的 Mark Word 更新为 "指向当前 Lock Record 的指针":

    • CAS 成功:线程获取轻量级锁,锁状态变为 00。
    • CAS 失败:说明有其他线程竞争,此时会先自旋(空循环)几次,尝试再次获取锁。
轻量级锁的膨胀:

若自旋失败(如自旋次数耗尽仍未获取锁,或有更多线程参与竞争),轻量级锁会 "膨胀" 为重量级锁:

  1. 将 Mark Word 中的指针改为 "指向 Monitor 的指针",锁状态变为 11。
  2. 未获取锁的线程进入 EntryList 阻塞(BLOCKED 状态),避免无效自旋浪费 CPU。

(3)重量级锁:激烈竞争场景的兜底

适用场景 :多个线程同时竞争锁,且竞争时间长(如线程持有锁执行耗时操作)。实现原理 :依赖操作系统的 互斥量(Mutex) 实现,线程竞争失败会直接阻塞(从用户态切换到内核态),等待被唤醒。特点:开销最大(线程阻塞 / 唤醒需内核态切换),但稳定性最高,适合激烈竞争场景。

2. 其他关键优化

(1)自旋锁与适应性自旋锁

  • 自旋锁:轻量级锁竞争时,线程不直接阻塞,而是自旋(空循环)一段时间(默认 10 次),等待持有锁的线程快速释放。自旋无需切换内核态,适合锁持有时间短的场景。

  • 适应性自旋锁:JDK 1.6 优化,自旋次数不再固定,而是根据 "历史自旋成功率" 动态调整:

    • 若之前自旋成功获取锁,下次自旋次数增加(如 20 次)。
    • 若之前自旋失败,下次自旋次数减少或直接放弃自旋(避免浪费 CPU)。

(2)锁消除

JIT 编译器在编译时,通过逃逸分析判断:若一个锁对象仅被当前线程访问(无逃逸到其他线程),则直接消除该锁。

  • 示例:StringBufferappend() 方法是同步方法,但单线程环境下,JIT 会消除其锁(因为 StringBuffer 对象未逃逸,无需同步)。

(3)锁粗化

若多个连续的锁操作针对同一个对象,JIT 会将这些分散的锁合并为一个 "粗粒度锁",减少锁的获取 / 释放次数。

  • 示例:循环中多次调用 synchronized (this) { ... },JIT 会将锁粗化到循环外部,仅获取一次锁即可。

三、synchronized 如何保证可见性,有序性,原子性

synchronized 是 Java 中唯一能同时保证 原子性、可见性、有序性 的内置同步机制,其保障逻辑完全基于底层的 Monitor 监视器锁JMM(Java 内存模型)规则,与之前提到的底层原理(如锁获取 / 释放、对象头操作)深度绑定。下面分三个特性,拆解其具体保障机制:

1、保证原子性:基于 Monitor 的互斥执行

1.1. 原子性的定义

原子性指 一个操作或一组操作,要么全部执行且执行过程不被打断,要么全部不执行 (不可分割)。比如 i++ 本质是「读取 i → 加 1 → 写入 i」三个步骤,若不加同步,多线程环境下可能被其他线程打断,导致结果错误。

1.2. synchronized 如何保证原子性?

核心逻辑:通过 Monitor 锁的互斥性,确保同步块 / 方法在同一时间只能被一个线程执行

结合底层原理的细节:

  • 线程要进入同步块 / 方法,必须先获取 Monitor 的所有权(通过 monitorenter 指令或 ACC_SYNCHRONIZED 标志)。
  • Monitor 的 Owner 字段同一时间只能指向一个线程(互斥性),其他竞争线程会被阻塞在 EntryList(BLOCKED 状态),直到当前线程释放锁。
  • 同步块 / 方法内的所有操作,会作为一个 "整体" 被执行 ------ 在当前线程释放锁前,其他线程无法插入执行,因此这组操作具备不可分割的原子性。

示例验证:

arduino 复制代码
private int count = 0;
private synchronized void increment() {
    count++; // 读取、加1、写入三个步骤,被synchronized保证为原子操作
}
  • 若不加 synchronized,1000 个线程各调用 1000 次 increment(),最终 count 会小于 1000000(因步骤被打断)。
  • synchronized 后,count++ 的三个步骤被 "串行化",最终结果必然是 1000000,原子性得到保证。

补充:可重入性不破坏原子性

synchronized 是可重入锁(同一线程可多次获取同一锁),但多次获取不会打破互斥性 ------ 其他线程仍需等待当前线程完全释放锁(所有重入的锁都释放)才能竞争,因此原子性依然成立。

2、保证可见性:基于锁释放 / 获取的内存刷新规则

2.1. 可见性的定义

可见性指 一个线程修改了共享变量的值后,其他线程能立刻感知到这个修改。若没有可见性保障,线程 A 修改的变量可能只存在于自己的工作内存中,未同步到主内存,线程 B 读取的仍是主内存中旧值。

2.2. synchronized 如何保证可见性?

核心逻辑:JMM 为 synchronized 定义了「锁释放 - 获取的内存语义」,强制刷新共享变量的内存

具体规则(与底层锁操作绑定):

  • 锁释放时:强制刷新工作内存到主内存 线程释放 Monitor 锁时(执行 monitorexit 指令,或方法执行完毕),JVM 会触发一个动作:将该线程在工作内存中修改的所有共享变量,强制刷新到主内存(清空工作内存中的缓存,确保主内存是最新值)。
  • 锁获取时:强制从主内存加载最新值 线程获取 Monitor 锁时(执行 monitorenter 指令,或调用同步方法),JVM 会触发一个动作:将该线程工作内存中对应的共享变量 置为无效,后续读取该变量时,必须从主内存重新加载(确保拿到的是最新值)。

示例验证:

arduino 复制代码
private boolean flag = false;
private synchronized void setFlag() {
    flag = true; // 线程A修改后,释放锁时刷新到主内存
}
private synchronized void checkFlag() {
    if (flag) { // 线程B获取锁时,从主内存加载最新的flag(true)
        System.out.println("感知到flag修改");
    }
}
  • 若不加 synchronized,线程 A 修改变量后可能未刷新到主内存,线程 B 一直读取工作内存中的旧值 false,无法感知修改。
  • synchronized 后,锁释放 / 获取的内存语义确保了 flag 的修改对其他线程可见。

3、保证有序性:基于互斥执行 + happens-before 规则

3.1. 有序性的定义

有序性指 程序执行的顺序与代码编写的顺序一致,避免因「指令重排序」导致的多线程执行混乱。JIT 编译器、CPU 为了优化性能,会在不影响单线程执行结果的前提下重排序指令,但多线程环境下可能导致错误。

3.2. synchronized 如何保证有序性?

核心逻辑:通过「互斥执行的串行化」和「JMM 的 happens-before 规则」,间接禁止指令重排序的可见性

(1)互斥执行的串行化保障

由于同步块 / 方法同一时间只能被一个线程执行,相当于将多线程执行转化为「单线程串行执行」。而单线程环境下,指令重排序不会影响执行结果(as-if-serial 语义)------ 无论指令如何重排,单线程执行的最终结果与代码顺序一致,因此多线程通过 synchronized 执行时,不会出现因重排序导致的逻辑错误。

(2)happens-before 规则的强约束

JMM 定义了「监视器锁规则」:对同一个锁的解锁操作(unlock),happens-before 于后续对该锁的加锁操作(lock)

  • happens-before 的含义:若操作 A happens-before 操作 B,则 A 的执行结果对 B 可见,且 A 的执行顺序在 B 之前(禁止 A 之后的指令重排序到 A 之前,也禁止 B 之前的指令重排序到 B 之后)。

  • 具体效果:同步块内的所有操作(解锁前的操作),happens-before 于后续获取该锁的线程执行的操作(加锁后的操作)。这意味着:

    • 同步块内的指令可以重排序,但不会重排序到同步块外部(解锁后)。
    • 后续线程获取锁后,能看到前一个线程同步块内所有操作的结果(包括指令重排序后的正确结果)。

示例验证(避免重排序问题):

arduino 复制代码
private int a = 0;
private boolean ready = false;

// 线程A执行
private synchronized void write() {
    a = 1;      // 操作1
    ready = true;// 操作2
}

// 线程B执行
private synchronized void read() {
    if (ready) { // 操作3
        System.out.println(a); // 操作4,必然输出1
    }
}
  • 若不加 synchronized,CPU 可能将线程 A 的「操作 1 和操作 2」重排序(先执行 ready=true,再执行 a=1),导致线程 B 执行时 ready=truea=0,输出错误。

  • synchronized 后:

    1. 线程 A 的同步块内,操作 1 和操作 2 可重排序,但不会重排序到 write() 方法外部(解锁后)。
    2. 「线程 A 解锁」happens-before「线程 B 加锁」,因此线程 B 执行时,必然能看到线程 A 操作 1 和操作 2 的最终结果(a=1ready=true),输出正确。

4、总结:三个特性的核心保障逻辑

特性 核心保障机制
原子性 Monitor 锁的互斥性:同步块 / 方法同一时间仅一个线程执行,操作不可分割。
可见性 锁释放 / 获取的内存语义:释放锁时刷新工作内存到主内存,获取锁时从主内存加载最新值。
有序性 1. 互斥执行转化为单线程串行(as-if-serial 语义);2. happens-before 规则约束指令重排序。

四、总结

1. 底层原理核心

synchronized 基于 Monitor 监视器锁对象头 Mark Word 实现,通过竞争 Monitor 所有权保证线程互斥,锁状态存储在 Mark Word 中。

2. 优化机制核心

JDK 1.6+ 的优化核心是 "锁升级" :从偏向锁(无竞争)→ 轻量级锁(轻度竞争,自旋)→ 重量级锁(激烈竞争,阻塞),按需降低开销;配合自旋锁、锁消除、锁粗化等优化,让 synchronized 兼顾安全性和高性能。

3. 性能对比

  • 无竞争场景:偏向锁 > 轻量级锁 > 重量级锁(偏向锁几乎无开销)。
  • 轻度竞争场景:轻量级锁(自旋)> 重量级锁(避免内核态切换)。
  • 激烈竞争场景:重量级锁(自旋无效,阻塞更高效)。
相关推荐
绝无仅有2 小时前
面试日志elk之ES数据查询与数据同步
后端·面试·架构
绝无仅有2 小时前
大场面试之最终一致性与分布式锁
后端·面试·架构
踏浪无痕5 小时前
@Transactional做不到的5件事,我用这6种方法解决了
spring boot·后端·面试
yoke菜籽6 小时前
面试150——区间
面试·职场和发展
Java天梯之路7 小时前
上篇讲坑,这篇讲 “根”!Java 数据类型底层逻辑全解析
java·面试
川Princess1 天前
【面试经验】百度Agent架构研发工程师一面
面试·职场和发展·架构·agent
uhakadotcom1 天前
Next.js 从入门到精通(1):项目架构与 App Router—— 文件系统路由与目录结构全解析
前端·面试·github
用户12039112947261 天前
面试官最爱问的字符串反转:7种JavaScript实现方法详解
算法·面试
南山安1 天前
从反转字符串看透面试官的“内心戏”:你的算法思维到底怎么样?
javascript·算法·面试