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

前言
在 Java 并发编程中,
synchronized是历史最悠久、使用最广泛的同步机制。在 JDK 1.6 之前,它被称为"重量级锁",性能较差;但在 JDK 1.6 对其进行了大规模优化(引入偏向锁、轻量级锁)后,其性能已足以应对绝大多数场景。
一、synchroinzed是什么?
synchronized 是 Java 提供的一个关键字,用于实现互斥同步(Mutual Exclusion)。它保证了被包裹的代码块在同一时刻只能被一个线程执行。
它主要提供三大并发特性:
-
原子性 (Atomicity): 确保代码块作为一个整体执行,中间不会被打断。
-
可见性 (Visibility): 解锁前必须将共享变量的最新值刷新到主内存,加锁前必须清空工作内存中共享变量的值,从而从主内存加载。
-
有序性 (Ordering): 保证代码块内部的指令虽然可以重排序,但对外部来看是串行执行的。
三种基本用法:
-
修饰实例方法: 锁是当前对象实例 (
this)。 -
修饰静态方法: 锁是当前类的
Class对象。 -
修饰代码块: 锁是括号内指定的对象。
二、对象头
要理解 synchronized 的底层,必须先理解 Java 对象头。在 HotSpot 虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
synchronized 用的锁是存在对象头里的。对象头中包含两部分数据:
-
Mark Word (标记字段): 存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志 、线程持有的锁、偏向线程 ID等。
-
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() {
// ...
}
反编译同步方法,通常不会 看到 monitorenter 和 monitorexit 指令。
核心原理:
-
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: 计数器。
工作流程:
-
线程尝试获取锁,如果成功,将
_owner设为自己。 -
如果失败,进入
_EntryList阻塞等待。 -
如果持有锁的线程调用
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)
-
场景: 多线程交替执行同步块,没有这种"你死我活"的激烈竞争(即锁持有时间短,且没有同时竞争)。
-
原理:
-
线程在栈帧中创建锁记录 (Lock Record)。
-
将对象头的 Mark Word 复制到锁记录中(Displaced Mark Word)。
-
使用 CAS (Compare And Swap) 尝试将对象头的 Mark Word 替换为指向锁记录的指针。
-
成功: 获得锁。
-
失败: 说明有竞争。
-
-
自旋 (Spinning): 失败后不会立即挂起线程(避免内核态切换),而是进行自旋(空循环)。如果自旋几次后锁释放了,就立刻抢锁;如果自旋多次仍未获得,则升级。
5.3 重量级锁
-
场景: 锁竞争激烈,或者持有锁的线程执行时间很长。
-
原理: 也就是前文提到的
ObjectMonitor和操作系统的Mutex Lock。未抢到锁的线程会被真正的挂起(阻塞)。
六、锁消除与锁粗化
除了锁升级,JVM 还有其他的优化手段:
-
锁消除 (Lock Elimination): JIT 编译时,通过逃逸分析,如果发现某个对象只在当前线程使用,根本不会被其他线程访问,就会把这里的
synchronized去掉。 -
锁粗化 (Lock Coarsening): 如果检测到一系列连续的锁操作是对同一个对象(比如在循环里加锁),JIT 会将加锁范围扩展到整个序列外部(循环外),减少加锁解锁次数。
总结
synchroized是Java之中很常用的锁,在底层实现是依靠Monitor,在JDK1.6之后做了大量优化,也是面试常考的内容。
