Java synchronized底层原理深度解析
在Java中,
synchronized
是用于实现线程同步的重要关键字,它的主要作用是控制访问共享资源的线程的数量。在并发编程中,理解synchronized
底层是非常重要的,它不仅能帮助我们避免死锁和竞态条件,还能帮助我们优化程序性能。在本篇博客中,我们将深入探讨synchronized
底层的实现原理,涵盖以下几个主要概念:对象头、Monitor、偏向锁、轻量级锁、重量级锁、锁膨胀以及自旋优化等
我们都知道,synchronized`是用于实现线程同步的重要关键字,防止出现线程安全问题,我们俗称他为锁:
synchronized(//锁对象) {// 临界区
}
这里的obj就是锁对象
那么现在我们将讨论他的加锁过程以及原理
对象头和Monitor
对象头
在Java中,所有的对象都有一个对象头(Object Header),它存储了与对象管理和同步相关的信息。对象头主要由两部分组成:Mark Word和Class Pointer。Mark Word用于存储与锁相关的信息,而Class Pointer是指向标识该类类型的指针,两部分各占32位即8个字节
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Class Pointer (32 bits) |
|------------------------------------|-------------------------|
其中Mark Word有几种不同的状态,他们的结构为:
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
biased_lock
用来表示是否是偏向锁状态,除此之外,各种状态可以用最后两位来区分
- 00:表示轻量级锁
- 10:表示重量级锁
- 01:表示正常状态或者是偏向锁状态
那么这些锁都是什么呢,我们逐一讨论:
Monitor
Monitor是每个对象都有的同步机制,负责管理对象的锁。每个对象对应一个Monitor,这个Monitor不仅用来管理锁,还用于线程之间的协调。当一个线程试图获取某个对象的锁时,它需要与这个对象的Monitor进行交互。Monitor内部维护了锁的状态,具体锁的实现取决于不同的锁类型(偏向锁、轻量级锁、重量级锁等)
他的结构如下:
使用synchronized(obj)加锁时,obj对象所对应的Monitor对象就会发生改变:
刚开始 Monitor 中 Owner 为 null,当有线程Thread1执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread1,Monitor中只能有一个 Owner,就代表Thread1线程获取了锁,同时也会通过CAS操作修改obj对象头中的Mark Word,其状态为10,前三十位存放Monitor对象的指针
在 Thread1上锁的过程中,如果有其他线程Thread2,Thread3,Thread4 也来执行 synchronized(obj),此时他们就会进入EntryList阻塞队列中变成阻塞状态,直到Thread1线程释放锁后,这些阻塞的线程再去竞争获取锁
WaitSet中放的是之前获得过锁,但条件不满足进入 WAITING 状态的线程
这种加锁方式就是重量级锁
但是这种加锁方式在线程竞争很小的时候,同一个线程每次获取锁都要经过十分复杂的CAS操作,于是为了减少性能开销,java6之后便有了不同的默认加锁类型:偏向锁和轻量级锁
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,自旋优化通过在获取锁时进行忙等待(即让线程处于不断循环的状态),而不是直接让线程挂起并进行上下文切换。这样可以避免因锁竞争而带来的线程切换开销,提高系统的整体性能
-
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
-
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
-
Java 7 之后不能控制是否开启自旋功能
轻量级锁
轻量级锁并没有使用专门的 Monitor 对象来控制线程对对象的访问,而是每次使用synchronized(obj) 获取锁时在线程的栈帧中生成一个锁记录Lock Record,其结构如图:
假设有一段代码:
static final Object obj = new Object();
public static void method1() {synchronized( obj ) {// 同步块 Amethod2();}
}
public static void method2() {synchronized( obj ) {// 同步块 B}
}
1️⃣当method1第一次执行synchronized( obj )
时,就会在当前线程的栈帧中生成一个锁记录Lock Record,并且让锁记录中 Object reference 指向锁对象obj,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录,如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00,而锁记录中的信息则替换为Mark Word 的值,表示由该线程给对象加锁,而第一部分中提到,00表示轻量级锁
2️⃣method1中接着又调用了method2,又一次运行了synchronized( obj )
,但是此时第一次加的锁还未释放,当查看obj的对象头时,发现对象头中存储的锁记录地址就是当前线程的,也就代表着当前线程想要重入锁,此时在栈帧中就会再生成一个锁记录Lock Record,但不会再次进行CAS替换操作,因为对象头中已经存储了锁记录的地址,而是直接将锁记录的第一部分设为null
3️⃣method2执行完毕释放锁时,由于锁记录中值为null,表示有重入,这时重置锁记录,表示重入计数减1,当method1退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
轻量级锁适用于交替执行的场景,轻量级锁 并不使用 Monitor 对象,它是通过在 Mark Word 中存储锁的状态信息来管理锁的。当线程竞争较少时,轻量级锁可以避免操作系统的 Monitor 锁,从而减少线程阻塞和上下文切换带来的性能开销
偏向锁
偏向锁是JVM为减少同步带来的性能开销而引入的一种优化机制。它假设在大多数情况下,某个对象的锁只会被同一个线程获取
当一个线程第一次获得锁时,JVM会在Mark Word中存储当前线程的ID,并将锁的状态设置为偏向锁。这样,后续如果同一个线程再次访问该锁时,它将不再进行同步操作,直接进入临界区。只有当其他线程尝试获取锁时,偏向锁才会被撤销,转为轻量级锁或其他类型的锁,对于单线程访问的情况,偏向锁几乎没有性能损失,因此能够显著提高程序的性能
这也是java6之后默认开启的锁类型
一个对象创建时:
-
如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
-
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数
-XX:BiasedLockingStartupDelay=0
来禁用延迟 -
如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
也就是说使用synchronized做线程同步时默认优先使用的就是偏向锁,如果此时发生了多线程竞争同一个锁对象时,就会发生锁膨胀
锁膨胀
指的是 轻量级锁(Lightweight Locking)在多线程竞争时升级为 重量级锁(Heavyweight Locking)的过程。这是 Java 中的一个优化机制,旨在尽量避免高竞争情况下带来的性能损失,同时保持线程同步的正确性
锁膨胀的触发条件
锁膨胀通常发生在 轻量级锁 的 CAS 操作(Compare and Swap)无法成功时。具体触发条件如下:
- 锁的竞争激烈:当多个线程同时竞争同一个锁时,轻量级锁的 CAS 操作会失败,因为每个线程都试图修改对象的 Mark Word(对象头中的一部分)以获取锁。如果 CAS 操作失败,说明该对象的锁已经被其他线程占用,这时轻量级锁会升级为重量级锁。
- CAS 失败:轻量级锁依赖于 CAS 操作。如果多个线程同时竞争同一把锁,CAS 操作会因为竞争失败而触发锁膨胀。由于 CAS 操作是原子性的,因此只有一个线程能够成功更新对象的 Mark Word
过程示意图: