Java中的锁 Synchronized 升级
(1)引言
在 JDK 1.5 之前,synchronized 的底层实现都是重量级的,借助操作系统底层实现,也称之为 synchronized 为重量级锁。在 JDK 1.5 之后,对 synchronized 进行了各种优化,实现的原理是锁升级的过程,有了偏向锁、轻量级锁和重量级锁的概念。
(2)Java 对象的内存布局
在 Java 中,创建一个对象后,在 JVM 中,对象在内存中的存储布局分为三块:
- 对象头区域:存放锁信息、对象年龄等信息。
- 实例数据区域:存储的是对象的真正有效的信息,比如对象中所有字段内容。
- 对齐填充区域:JVM 规定对象的起始地址必须是 8 字节的整数倍。
synchronized 用的锁是存在对象头里的。在 Java 对象头中 Mark Word 是默认存储对象的 hashcode、分代年龄和锁的标记位。
在 Java SE 1.6 中,锁一共存在 4 种状态,级别从低到高依次是:无锁状态 → 偏向锁状态 → 轻量级锁状态 → 重量级锁状态,这几个状态随着竞争的情况逐渐升级,锁可以升级但不能降级。
(3)偏向锁
偏向锁的操作是无需操作系统介入的。每个对象都有对象头,对象头中的 Mark Word 区域存储对象的锁信息。
该对象头先处于无锁状态,当有线程来访问,JVM 使用 CAS 操作将线程 ID 记录到 Mark Word 中,修改偏向锁的标识位,当前线程就拥有了这把锁。
注意:将线程 ID 通过 CAS 记录,变更偏向锁标识为 1。
优点: 只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获取同一个锁的情况。
(4)轻量级锁
如果在偏向锁中线程 A 一直执行过程中,此时又来了另一个线程 B 要进入代码块中执行,但锁对象保存的是线程 A 的线程 ID,这时候就需要对偏向锁进行升级,变成一个轻量级锁。
JVM 把锁对象恢复成无锁状态,在两个线程的栈帧中各分配一个空间,叫做 Lock Record,把锁对象的 Mark Word 在两个线程的栈帧中各复制一份,叫做 Displaced Mark Word。将当前线程 A 的 Lock Record 的地址使用 CAS 放到锁对象的 Mark Word 当中,并且将锁的标识设置为 00。
线程 B 没有获取到锁,但不阻塞,JVM 会让他自旋几次,等待一会儿。
优点: 绝大部分的锁在整个生命周期中都存在少量竞争,在多线程交替执行同步代码块时可以避免重量级锁引起的性能问题。
(5)重量级锁
轻量级锁在运行时,线程 A 正在持有锁,另一个线程 B 自旋了好多次,线程 A 还没有释放锁,这个时候 JVM 考虑自旋次数太多浪费 CPU 资源,就需要将锁升级为重量级锁。
重量级锁需要操作系统的介入,依赖操作系统底层的 mutex lock,JVM 会创建一个 monitor 对象,把这个对象的地址信息更新到 Mark Word 中,并将锁标志置为 10。线程 A 还在持有锁运行,线程 B 直接挂起,线程进入阻塞。
Synchronized 核心优化方案
synchronized 核心优化方案主要包含以下 4 个:
1. 锁膨胀
所谓的锁膨胀是指 synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,也叫做锁升级。
JDK 1.6 之前,synchronized 在释放和获取锁时都会从用户态转换成内核态。有了锁膨胀机制之后,大部分场景都不需要用户态到内核态的转换了,大幅提升了 synchronized 的性能。
2. 锁消除
锁消除指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉。
锁消除的依据是逃逸分析的数据支持,如 StringBuffer 的 append() 方法,或 Vector 的 add() 方法,在很多情况下可以进行锁消除。
public String method() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10; i++) {
sb.append("i:" + i);
}
return sb.toString();
}
以上代码经过编译之后,StringBuffer 被替换成了不加锁不安全的 StringBuilder 对象。
3. 锁粗化
锁粗化是指将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
// 粗化前:每次循环都加解锁
for (int i = 0; i < 10; i++) {
// 加锁
sb.append(i);
// 解锁
}
// 粗化后:整个循环一把锁
// 加锁
for (int i = 0; i < 10; i++) {
sb.append(i);
}
// 解锁
4. 自适应自旋锁
自旋锁是指通过自身循环,尝试获取锁的一种方式。
// 伪代码:自旋获取锁
while (!isLock()) { }
对于 synchronized 关键字来说,它的自旋锁更加"智能"——自适应自旋锁,线程自旋的次数不再是固定的值,而是一个动态改变的值:
- 如果上次自旋成功获取到锁 → 这次自旋次数会增多
- 如果上次自旋没有成功获取到锁 → 这次自旋次数会减少或不循环
总结
- 锁膨胀 和 自适应自旋锁 是 synchronized 关键字自身的优化实现
- 锁消除 和 锁粗化 是 JVM 虚拟机对 synchronized 提供的优化方案
这些优化方案最终使得 synchronized 的性能得到了大幅提升,也让它在并发编程中占据了一席之地。