synchronized的升级过程

126 阅读13分钟

关键点

  • 锁升级过程:Java 的 synchronized 关键字通过无锁、偏向锁(已弃用)、轻量级锁和重量级锁的升级机制优化性能,适应不同竞争场景。
  • 轻量级锁与重量级锁:轻量级锁运行在用户态(Ring 3),使用 CAS 和自旋,适合低竞争;重量级锁运行在内核态(Ring 0),依赖操作系统互斥锁,适合高竞争。
  • 阈值调整:JVM 自动管理锁升级阈值,开发者可通过标志如 -XX:+DisableFatLockSpin 调整重量级锁行为,但无法直接设置升级阈值。
  • 特权级:轻量级锁在 Ring 3(用户态)运行,重量级锁涉及 Ring 0(内核态),Ring 1 和 Ring 2 在现代系统中几乎不使用。
  • 偏向锁现状:偏向锁在 Java 15 及以上版本默认禁用,已被标记为弃用。

synchronized 的作用

Java 的 synchronized 关键字用于实现线程同步,确保多线程环境下共享资源的互斥访问,同时提供 happens-before 关系以保证内存可见性。它通过对象的内置锁(也称为监视器锁)实现同步,适用于方法或代码块。

锁升级过程

Java 的 synchronized 锁升级机制是自适应的,JVM 根据线程竞争情况动态调整锁状态,从无锁逐步升级到偏向锁(若启用)、轻量级锁和重量级锁。这种机制旨在平衡性能和并发需求,锁升级是单向的,无法降级。

轻量级锁与重量级锁的区别

轻量级锁和重量级锁在运行模式、实现机制和适用场景上有显著差异:

  • 轻量级锁:运行在用户态(Ring 3),使用 CAS 操作和自旋,适合低竞争场景,性能开销低。
  • 重量级锁:运行在内核态(Ring 0),依赖操作系统互斥锁,适合高竞争场景,但涉及上下文切换,开销较高。

调整锁升级阈值

JVM 自动管理锁升级阈值(如从轻量级锁到重量级锁的转换),主要基于线程竞争程度和自旋次数。开发者无法直接设置具体阈值,但可以通过 JVM 标志调整相关行为,例如 -XX:+DisableFatLockSpin 禁用重量级锁的自旋,使线程立即阻塞。

特权级与锁机制

x86 体系结构的特权级(Ring 0 到 Ring 3)定义了代码的执行权限:

  • Ring 0(内核态) :重量级锁依赖操作系统互斥锁,涉及系统调用。
  • Ring 3(用户态) :轻量级锁和偏向锁运行在此,依赖 JVM 的 CAS 和自旋。
  • Ring 1 和 Ring 2:现代操作系统中几乎不使用,与 synchronized 无直接关联。

synchronized的升级过程详解

1. 引言

在 Java 中,synchronized 关键字是实现线程同步的核心工具,用于确保多线程环境下共享资源的安全访问。它通过对象的内置锁(也称为监视器锁)实现互斥访问,同时建立 happens-before 关系,确保线程间的内存可见性。synchronized 可以应用于方法或代码块,广泛用于防止竞争条件和数据不一致问题。

Java 的 synchronized 锁机制基于对象头中的 Mark Word,JVM 通过动态调整锁状态(无锁、偏向锁、轻量级锁、重量级锁)来优化性能。这种锁升级机制(也称为锁膨胀过程)是 Java HotSpot JVM 的关键优化,旨在根据线程竞争情况选择合适的锁类型,平衡性能和并发需求。

本文将详细介绍 synchronized 的锁升级过程,阐释每个锁状态的含义,特别说明轻量级锁和重量级锁的区别,并探讨如何调整锁升级阈值以及锁机制与 CPU 特权级(Ring 0 到 Ring 3)的关系。

2. 锁升级过程

Java 的 synchronized 锁升级机制遵循以下路径:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

锁升级是单向的,JVM 不会将高级锁降级为低级锁。以下是每个锁状态的含义和适用场景:

2.1 无锁 (No Lock)

  • 含义:对象未被任何线程锁定,Mark Word 存储对象的哈希码、GC 分代年龄等信息。
  • 场景:没有线程竞争,访问完全自由。
  • 性能:无同步开销,性能最高。
  • 实现:无需锁机制,线程直接访问对象。

2.2 偏向锁 (Biased Lock)

  • 含义:当一个线程首次访问某个对象时,JVM 将该线程的 ID 记录到对象的 Mark Word 中,使对象“偏向”于该线程。后续该线程访问时无需额外同步操作。
  • 场景:适用于单线程反复访问同一对象的情况,例如单线程操作共享对象。
  • 性能:开销极低,运行在用户态(Ring 3),无需操作系统干预。
  • 实现:通过修改 Mark Word 实现偏向,撤销偏向锁(当其他线程竞争时)会有一定开销。
  • 注意:根据 JEP 374,偏向锁在 Java 15 及以上版本默认禁用,并被标记为弃用。开发者可通过 -XX:+UseBiasedLocking 启用,但不推荐在未来版本中依赖。

2.3 轻量级锁 (Lightweight Lock)

  • 含义:当多个线程竞争同一对象但竞争不激烈(例如线程交替访问)时,偏向锁(若启用)或无锁升级为轻量级锁。轻量级锁使用 CAS(Compare-And-Swap) 操作,通过自旋(忙等待)尝试获取锁。
  • 场景:适用于低竞争场景,线程持有锁的时间较短。
  • 性能:运行在用户态(Ring 3),通过 CAS 和自旋实现,避免进入内核态,开销比偏向锁略高但仍较低。
  • 实现:Mark Word 存储指向线程栈中锁记录的指针,线程通过 CAS 操作尝试更新锁状态。如果自旋次数过多(由 JVM 自适应调整),锁会升级为重量级锁。

2.4 重量级锁 (Heavyweight Lock)

  • 含义:当线程竞争加剧(例如多个线程同时尝试获取锁)时,轻量级锁升级为重量级锁。重量级锁依赖操作系统的 互斥锁(Mutex) ,通过系统调用管理线程的阻塞和唤醒。
  • 场景:适用于高竞争场景,线程可能需要长时间等待。
  • 性能:运行在内核态(Ring 0),涉及用户态到内核态的切换、线程调度等,开销较高。
  • 实现:Mark Word 存储指向操作系统 Monitor(监视器)的指针,线程无法获取锁时会被阻塞,进入等待队列。

3. 轻量级锁与重量级锁的区别

轻量级锁和重量级锁是 synchronized 锁升级机制中的两个关键阶段,它们的区别直接影响性能和适用场景。以下是详细对比:

特性轻量级锁 (Lightweight Lock)重量级锁 (Heavyweight Lock)
运行模式用户态(Ring 3)内核态(Ring 0)
实现机制基于 CAS 操作和自旋,Mark Word 存储锁记录指针基于操作系统互斥锁,Mark Word 存储 Monitor 指针
性能开销较低,适合短时间竞争较高,涉及上下文切换和线程调度
适用场景低竞争,线程交替访问高竞争,线程长时间等待
线程行为自旋(忙等待),不阻塞线程阻塞线程,进入等待队列
线程状态通常为 RUNNABLE可能为 BLOCKED、WAITING 或 TIMED_WAITING

3.1 用户态与内核态

  • 轻量级锁:运行在用户态(Ring 3),依赖 JVM 的 CAS 操作和自旋机制。线程在获取锁失败时会短暂自旋,尝试多次 CAS 操作以获取锁,避免进入内核态。这种方式减少了系统调用的开销,适合竞争不激烈的场景。
  • 重量级锁:运行在内核态(Ring 0),依赖操作系统的互斥锁(如 POSIX 的 pthread_mutex 或 Windows 的 Critical Section)。线程获取锁失败时会被阻塞,进入内核态的等待队列,等待被唤醒。这种方式适合高竞争场景,但涉及用户态到内核态的切换,性能开销较大。

3.2 性能影响

  • 轻量级锁:通过自旋和 CAS 操作,减少了上下文切换的开销,适合锁持有时间短的场景。JVM 的自适应自旋(Adaptive Spinning)会根据历史竞争情况动态调整自旋次数,进一步优化性能。
  • 重量级锁:由于涉及系统调用和线程调度,性能开销较高,但通过阻塞线程避免 CPU 浪费,适合高竞争场景。

3.3 线程状态

  • 轻量级锁:线程通常处于 RUNNABLE 状态,通过自旋尝试获取锁,避免阻塞。
  • 重量级锁:线程可能进入 BLOCKED(等待锁)、WAITING(调用 wait())或 TIMED_WAITING(限时等待)状态,这些状态的转换由内核(Ring 0)管理。

4. 调整锁升级阈值

JVM 自动管理锁升级阈值(如从轻量级锁到重量级锁的转换),主要基于线程竞争程度和自旋次数。开发者无法直接设置具体的升级阈值,但可以通过 JVM 标志调整相关行为。

4.1 偏向锁相关标志(已弃用)

在 Java 15 之前,偏向锁是默认启用的,开发者可以通过以下标志调整其行为:

  • -XX:+UseBiasedLocking:启用偏向锁(Java 15 及以上默认禁用)。
  • -XX:BiasedLockingStartupDelay:设置偏向锁启用前的延迟时间(单位:毫秒),默认值为 4000ms。
  • -XX:BiasedLockingBulkRebiasThreshold:设置批量重新偏向的阈值。
  • -XX:BiasedLockingBulkRevokeThreshold:设置批量撤销偏向的阈值。

根据 JEP 374,偏向锁已被弃用,这些标志在未来版本中可能不再可用,开发者应避免依赖。

4.2 重量级锁相关标志

  • -XX:+DisableFatLockSpin:禁用重量级锁的自旋行为。当线程尝试获取重量级锁失败时,立即阻塞而非自旋等待。这可以减少 CPU 浪费,但可能增加上下文切换开销。

示例: 要禁用重量级锁的自旋行为,可以在启动 JVM 时添加以下标志:

java -XX:+DisableFatLockSpin YourJavaApplication

4.3 自适应自旋

JVM 使用自适应自旋(Adaptive Spinning)动态调整轻量级锁的自旋次数,根据历史竞争情况决定是否继续自旋或升级为重量级锁。没有直接标志设置自旋次数阈值,但开发者可以通过监控线程状态(如使用 JStack)分析性能瓶颈。

4.4 限制

目前没有直接的 JVM 标志允许开发者精确控制轻量级锁到重量级锁的升级阈值。JVM 内部根据竞争程度和运行时统计数据自动决定升级时机。这种自适应机制通常比手动配置更高效,但开发者可以通过调整上述标志间接影响锁行为。

5. 特权级(Ring 0 到 Ring 3)与 synchronized 的关联

x86 体系结构定义了四种特权级(Ring 0 到 Ring 3),用于隔离和保护操作系统及应用程序的执行环境。这些特权级与 synchronized 的锁机制在用户态和内核态的运行模式中有直接关联。

5.1 特权级概述

  • Ring 0(内核态) :运行操作系统的内核代码,具有最高权限,可以直接访问硬件资源(如 CPU、内存、I/O 设备)。重量级锁依赖操作系统的互斥锁,涉及 Ring 0 的系统调用,如线程调度和阻塞。
  • Ring 1 和 Ring 2(中间特权级) :理论上用于运行设备驱动或部分特权组件,但在现代操作系统(如 Windows、Linux)中几乎不使用,与 synchronized 无直接关联。
  • Ring 3(用户态) :运行用户应用程序代码,权限最低,只能通过系统调用请求内核服务。偏向锁(若启用)和轻量级锁运行在 Ring 3,依赖 JVM 的 CAS 操作和自旋机制。
  • Ring 4:x86 体系结构中不存在 Ring 4,特权级仅限于 Ring 0 到 Ring 3。

5.2 特权级与锁机制

  • 偏向锁和轻量级锁:运行在 Ring 3(用户态),通过 JVM 的内部机制(如 CAS 和自旋)实现同步,避免内核态切换,性能开销低。
  • 重量级锁:运行在 Ring 0(内核态),依赖操作系统的互斥锁,涉及系统调用和线程调度,性能开销较高。
  • 性能影响:轻量级锁在 Ring 3 的操作减少了上下文切换开销,适合低竞争场景;重量级锁在 Ring 0 的操作确保高竞争场景下的线程安全,但增加了系统调用开销。

5.3 线程状态与特权级

  • 轻量级锁:线程通常处于 RUNNABLE 状态,在 Ring 3 通过自旋竞争锁。
  • 重量级锁:线程可能进入 BLOCKED(等待锁)、WAITING(调用 wait())或 TIMED_WAITING(限时等待)状态,这些状态的转换由 Ring 0 的内核管理。

6. 锁升级的意义

synchronized 的锁升级机制是 Java HotSpot JVM 的一项性能优化,旨在根据运行时竞争情况动态选择合适的锁类型:

  • 性能优化:无锁和轻量级锁在用户态(Ring 3)运行,减少内核态(Ring 0)切换的开销。
  • 自适应性:JVM 通过自适应自旋等技术动态调整锁行为,优化性能。
  • 适用性:重量级锁通过阻塞线程避免 CPU 浪费,适合高竞争场景。

7. 注意事项

  • 偏向锁弃用:在 Java 15 及以上版本中,偏向锁默认禁用,开发者应避免依赖。使用 -XX:+UseBiasedLocking 可临时启用,但不推荐。
  • 自旋优化:轻量级锁的自旋次数由 JVM 自适应调整,过多自旋可能导致 CPU 浪费,触发升级到重量级锁。
  • 锁粗化与消除:JVM 可能通过锁粗化(合并多个同步块)或锁消除(移除无竞争的锁)进一步优化 synchronized 的性能。
  • 性能监控:开发者可通过 Thread.getState() 或工具(如 JStack)监控线程状态,分析 synchronized 块中的性能瓶颈。
  • 特权级隔离:Ring 0 和 Ring 3 的隔离确保了 synchronized 的安全性,重量级锁依赖 Ring 0 的内核机制,而轻量级锁利用 Ring 3 的高效性。

8. 结论

Java 的 synchronized 锁升级机制通过无锁、偏向锁(已弃用)、轻量级锁和重量级锁的动态切换,实现了性能与并发需求的平衡。轻量级锁运行在用户态(Ring 3),通过 CAS 和自旋支持低竞争场景;重量级锁运行在内核态(Ring 0),通过操作系统互斥锁管理高竞争场景。开发者无法直接设置锁升级阈值,但可以通过 JVM 标志(如 -XX:+DisableFatLockSpin)调整锁行为。特权级(Ring 0 到 Ring 3)定义了锁机制的运行环境,Ring 1 和 Ring 2 在现代系统中几乎不使用。理解锁升级、线程状态和特权级的关系有助于开发者编写高效的并发代码,同时依赖 JVM 的优化来确保性能。