菜单

Administrator
发布于 2026-05-15 / 3 阅读
0
0

Java中的锁Synchronized升级

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 的性能得到了大幅提升,也让它在并发编程中占据了一席之地。


评论