Java并发基础之Synchronized锁升级

141 阅读4分钟

1. java对象头

Java 对象头是 JVM 为每个对象在内存中分配的额外数据结构,存储了与对象相关的元信息。它位于对象内存布局的起始部分,是 JVM 管理对象的关键数据结构之一。 对象头主要由三类组成,第一类是Mark Word,它是 Java 对象头的动态部分,用于存储对象的运行时状态信息,根据锁的状态会发生变化。第二类是Class Pointer,它是一个指针,指向该对象的类元数据,JVM 通过它可以确定对象所属的类,包括对象对应的类的类型信息(如方法表、字段表等)。第三类只有对象为数组时才会存在,用来存储数组长度。三类中,我们重点关注第一类Mark Work,是理解java锁的核心。

锁状态1bit:是否偏向2bit:锁标志位
无锁25bit:hashcode4bit:分代年龄001
偏向锁23bit:线程id 2bit:时间戳4bit:分代年龄101
轻量级锁29bit:指向栈中锁记录指针00
重量级锁29bit:指向重量级锁的指针10
GC标记11

2.锁的升级与对比

偏向锁 (Biased Locking)

概念:

偏向锁是一种针对单线程访问场景的优化机制。它假设锁在大多数情况下是由同一线程访问的,因此避免了频繁加锁和解锁的操作。

适用场景:

单线程反复进入同步代码块。

实现原理:

• 锁的所有权会偏向第一个获得它的线程。

• 当这个线程再次进入同步代码块时,不需要进行加锁操作,只需检测 Mark Word 中记录的线程 ID 是否与当前线程匹配。

• 如果匹配,则不需要任何额外操作(类似于无锁)。

优点:

避免了 CAS 操作(Compare-And-Swap),性能较高。

撤销:

如果另一个线程尝试竞争该锁,偏向锁会被撤销,并升级为轻量级锁。

轻量级锁 (Lightweight Lock)

概念:

轻量级锁是一种针对短时间锁竞争的优化机制。通过 CAS 操作(而非阻塞)实现线程之间的锁竞争,避免了重量级锁带来的线程阻塞和上下文切换。

适用场景:

多线程交替短时间访问同步代码块,但线程之间的竞争不激烈。

实现原理:

• 在加锁时,JVM 会尝试将锁对象的 Mark Word 拷贝到当前线程的栈中(称为“锁记录”)。

• 然后通过 CAS 操作尝试将锁对象的 Mark Word 替换为指向锁记录的指针。

• 如果 CAS 成功,说明当前线程获得锁。

• 如果 CAS 失败,则表示有其他线程正在竞争,锁会升级为重量级锁。

优点:

线程不会阻塞,仅使用自旋等待,适用于锁竞争不激烈的场景。

缺点:

自旋需要占用 CPU,若竞争激烈会导致性能下降。

重量级锁 (Heavyweight Lock)

概念:

重量级锁是传统的锁机制,通过操作系统的互斥锁实现,线程阻塞和唤醒都需要操作系统的上下文切换。

适用场景:

多线程激烈竞争同步代码块。

实现原理:

• 当轻量级锁自旋失败时,锁会升级为重量级锁。

• 线程进入阻塞状态,等待其他线程释放锁。

• 使用操作系统的 Mutex(互斥锁)机制实现,阻塞线程会被挂起,等待唤醒。

优点:

避免了自旋造成的 CPU 资源浪费,适合高并发场景。

缺点:

线程阻塞和唤醒需要切换到内核态,性能较低。

3. 总结

锁的状态会随着并发竞争情况升级,且锁只能升级不能降级。

锁的状态流转

  1. 无锁(对象初始状态)。

  2. 偏向锁:单线程访问时,进入偏向状态。

  3. 轻量级锁:多线程访问发生竞争时,偏向锁撤销并升级为轻量级锁。

  4. 重量级锁:如果轻量级锁竞争过于激烈,升级为重量级锁。

锁类型适用场景优点缺点
偏向锁单线程访问减少加锁和解锁的开销需要撤销偏向锁,增加延迟
轻量级锁竞争不激烈的多线程场景无阻塞,自旋提高性能自旋占用CPU
重量级锁激烈竞争的多线程场景避免自旋浪费CPU线程阻塞,切换开销大