深度解析 Java synchronized 实现原理

大家好,这里是程序员阿亮,这俩天实在不知道记录点什么好,就日常看看原理,然后看到很久之前接触的synchronized的底层原理,就来这里再整理一下!

前言

在 Java 并发编程中,synchronized 是历史最悠久、使用最广泛的同步机制。在 JDK 1.6 之前,它被称为"重量级锁",性能较差;但在 JDK 1.6 对其进行了大规模优化(引入偏向锁、轻量级锁)后,其性能已足以应对绝大多数场景。

一、synchroinzed是什么?

synchronized 是 Java 提供的一个关键字,用于实现互斥同步(Mutual Exclusion)。它保证了被包裹的代码块在同一时刻只能被一个线程执行。

它主要提供三大并发特性:

  1. 原子性 (Atomicity): 确保代码块作为一个整体执行,中间不会被打断。

  2. 可见性 (Visibility): 解锁前必须将共享变量的最新值刷新到主内存,加锁前必须清空工作内存中共享变量的值,从而从主内存加载。

  3. 有序性 (Ordering): 保证代码块内部的指令虽然可以重排序,但对外部来看是串行执行的。

三种基本用法:

  • 修饰实例方法: 锁是当前对象实例 (this)。

  • 修饰静态方法: 锁是当前类的 Class 对象。

  • 修饰代码块: 锁是括号内指定的对象。

二、对象头

要理解 synchronized 的底层,必须先理解 Java 对象头。在 HotSpot 虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。

synchronized 用的锁是存在对象头里的。对象头中包含两部分数据:

  1. Mark Word (标记字段): 存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志 、线程持有的锁、偏向线程 ID等。

  2. Klass Pointer (类型指针): 对象指向它的类元数据的指针。

Mark Word 是实现锁升级的关键 。由于对象头信息是与对象自身定义的数据无关的额外存储成本,Mark Word 被设计成一个非固定的数据结构,以便在极小的空间内存存储尽量多的信息。它会根据对象的状态复用自己的存储空间(例如,在轻量级锁状态下,它存储指向栈中锁记录的指针 ;在重量级锁状态下,它存储指向互斥量(Monitor)的指针)。

三、底层实现

实际上本质都是尝试获取Monitor,成为Monitor的owner,只不过在synchronized的方法和代码块的字节码层面有所不同。每个对象都有自己关联的Monitor对象,这是C++底层实现的。

虽然都是使用 synchronized,但同步代码块同步方法在字节码层面的实现细节是不同的。

3.1 同步代码块 (Synchronized Block)

我们编写一段简单的代码:

java 复制代码
public void method() {
    synchronized (this) {
        System.out.println("Hello");
    }
}

通过 javap -c 反编译后,会看到如下指令:

复制代码
monitorenter          // 1. 进入同步块
...
monitorexit           // 2. 正常退出同步块
goto ...
monitorexit           // 3. 异常退出同步块(编译器自动生成)

核心原理:

  • monitorenter 每个对象都有一个监视器锁(Monitor)。当执行到该指令时,线程尝试获取对象的 Monitor 所有权。

    • 如果 Monitor 的计数器(count)为 0,则该线程获取锁,并将计数器设为 1。

    • 如果当前线程已经拥有该锁,则它是可重入的,计数器 +1。

    • 如果其他线程拥有该锁,当前线程会被阻塞,直到计数器变为 0。

  • monitorexit 执行该指令将 Monitor 计数器 -1。当计数器减为 0 时,锁被释放。

  • 异常处理: 编译器会自动生成一个异常处理器,确保即使同步块内抛出异常,monitorexit 也会被执行,防止死锁。

3.2 同步方法 (Synchronized Method)

java 复制代码
public synchronized void method() {
    // ...
}

反编译同步方法,通常不会 看到 monitorentermonitorexit 指令。

核心原理:

  • ACC_SYNCHRONIZED 标志: JVM 通过常量池中的方法表结构(Method_Info)里的 ACC_SYNCHRONIZED 访问标志来区分是否是同步方法。

  • 当方法被调用时,调用指令会检查该标志。如果设置了,执行线程将先隐式地获取 Monitor,执行方法体,方法执行完(或异常退出)后释放 Monitor。

  • 本质上与同步块相同,只是由 JVM 方法调用机制来隐式处理。

四、Monitor

在 JDK 1.6 之前,synchronized 主要是重量级锁 。当锁膨胀为重量级锁时,Java 对象头的 Mark Word 会指向一个 ObjectMonitor 对象(C++ 实现,位于 HotSpot 源码中)。

ObjectMonitor 的关键字段如下(简化版):

  • _owner 指向持有 ObjectMonitor 的线程。

  • _WaitSet 存放处于 WAITING 状态的线程队列(调用了 wait() 方法)。

  • _EntryList 存放处于 BLOCKED 状态的线程队列(在等待锁)。

  • _recursions 锁的重入次数。

  • _count 计数器。

工作流程:

  1. 线程尝试获取锁,如果成功,将 _owner 设为自己。

  2. 如果失败,进入 _EntryList 阻塞等待。

  3. 如果持有锁的线程调用 wait(),则释放锁,进入 _WaitSet 等待被 notify()

为什么叫"重量级"? 重量级锁依赖于操作系统的 Mutex Lock(互斥锁) 实现。

  • Java 的线程是映射到操作系统的原生线程之上的。

  • 挂起和唤醒一个线程,需要操作系统从用户态 (User Space) 切换到 内核态 (Kernel Space)

  • 这种状态转换非常消耗 CPU 资源,甚至比执行同步代码本身的时间还要长。

五、JDK1.6的优化

为了解决重量级锁性能低下的问题,JDK 1.6 引入了锁升级 机制。锁的状态会随着竞争情况逐渐升级,但不能降级(部分特定GC场景除外)。

锁的状态流转:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

5.1 偏向锁 (Biased Locking)

  • 场景: 只有一个线程访问同步块,不存在多线程竞争。

  • 原理: 当线程第一次访问同步块时,CAS 操作将线程 ID 记录到对象头的 Mark Word 中。以后该线程再进入时,无需任何同步操作,直接检查 Mark Word 里的 ID 是否是自己即可。

  • 优势: 几乎没有额外开销。

  • 撤销: 一旦有第二个线程尝试获取锁,偏向模式宣告结束,升级为轻量级锁。

5.2 轻量级锁 (Lightweight Locking)

  • 场景: 多线程交替执行同步块,没有这种"你死我活"的激烈竞争(即锁持有时间短,且没有同时竞争)。

  • 原理:

    1. 线程在栈帧中创建锁记录 (Lock Record)

    2. 将对象头的 Mark Word 复制到锁记录中(Displaced Mark Word)。

    3. 使用 CAS (Compare And Swap) 尝试将对象头的 Mark Word 替换为指向锁记录的指针。

    4. 成功: 获得锁。

    5. 失败: 说明有竞争。

  • 自旋 (Spinning): 失败后不会立即挂起线程(避免内核态切换),而是进行自旋(空循环)。如果自旋几次后锁释放了,就立刻抢锁;如果自旋多次仍未获得,则升级。

5.3 重量级锁

  • 场景: 锁竞争激烈,或者持有锁的线程执行时间很长。

  • 原理: 也就是前文提到的 ObjectMonitor 和操作系统的 Mutex Lock。未抢到锁的线程会被真正的挂起(阻塞)。

六、锁消除与锁粗化

除了锁升级,JVM 还有其他的优化手段:

  • 锁消除 (Lock Elimination): JIT 编译时,通过逃逸分析,如果发现某个对象只在当前线程使用,根本不会被其他线程访问,就会把这里的 synchronized 去掉。

  • 锁粗化 (Lock Coarsening): 如果检测到一系列连续的锁操作是对同一个对象(比如在循环里加锁),JIT 会将加锁范围扩展到整个序列外部(循环外),减少加锁解锁次数。


总结

synchroized是Java之中很常用的锁,在底层实现是依靠Monitor,在JDK1.6之后做了大量优化,也是面试常考的内容。

相关推荐
寻寻觅觅☆13 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
l1t14 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
青云计划14 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿14 小时前
Jsoniter(java版本)使用介绍
java·开发语言
ceclar12315 小时前
C++使用format
开发语言·c++·算法
探路者继续奋斗15 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
码说AI15 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS15 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
星空下的月光影子15 小时前
易语言开发从入门到精通:补充篇·网络爬虫与自动化采集分析系统深度实战·HTTP/HTTPS请求·HTML/JSON解析·反爬策略·电商价格监控·新闻资讯采集
开发语言
老约家的可汗15 小时前
初识C++
开发语言·c++