synchronized 重量级锁的核心原理详解

在这里,我抛开一些细节术语,用更直接的方式来阐述 synchronized 的本质原理。实际上,synchronized 的原理并不复杂,很多时候是被各种包装概念和底层源码的畏惧感所阻碍。本文仅探讨重量级锁的原理,暂不涉及锁升级过程。

  • synchronized 的目标

首先我们不要掉书袋,只需要明确synchronized 的核心目标只有一个:确保在同一时刻,只有一个线程能够执行特定的代码块或方法。

我们可以用现实生活中的场景来理解:假设要保证同一时刻只有一个能进入房间,最直接的方法就是设计一把唯一的钥匙(锁对象)。只有使用这把特定的钥匙(锁对象)插入钥匙孔(与对象关联的 Monitor)并旋转,才能打开门锁机制,进入房间(执行同步代码)。而其他没有这把钥匙的人,会被门锁机制(Monitor)挡在门外,需要等待持有钥匙的人(线程)出来并锁上门(释放 Monitor 控制权)后,才有机会尝试拿到钥匙开门。

  • 如何实现这个目标?

为了实现上述目标,我们需要记录两个关键信息:

  1. 这把锁当前被哪个线程持有。
  2. 哪些线程正在等待这把锁。

实现这些信息的关键结构是:Monitor锁对象的对象头 Mark Word

锁对象:这是用户层面上的概念。当你在 Java 代码中使用 synchronized 关键字时,你需要指定一个对象作为锁,这个对象就是锁对象。每个锁对象的对象头中都包含一个名为 Mark Word 的字段,用于记录一些重要的对象信息,例如哈希码、GC 分代年龄、锁标志以及偏向锁标识等。

Monitor:这是 JVM 层面上的概念。Monitor 是 JVM 内部实现线程同步的一种机制。在 JVM 内部,每个 Java 对象都可能关联着一个 Monitor 对象。

  • Monitor 和 Mark Word

初看 MonitorMark Word 的结构可能会觉得复杂,但其核心作用是相对简单的。

Monitor 的基本结构(逻辑视图):

+-----------------------+
|        _owner         |  // 指向持有该 Monitor 的线程
|       (Thread*)       |
+-----------------------+
|      _entryList       |  // 存放等待获取锁的线程队列 (Entry Set)
|    (ObjectWaiter*)    |
+-----------------------+
|       _WaitSet        |  // 存放调用 wait() 进入等待状态的线程队列
|    (ObjectWaiter*)    |
+-----------------------+
|       _count          |  // 记录线程重入锁的次数
|        (int)          |
+-----------------------+
|    ... _header ...    |  // 备份原始对象的 Mark Word
+-----------------------+
|    ... 其他字段 ...    |  // 例如:记录 Monitor 状态、相关标志位等
+-----------------------+

注意,Monitor 的具体实现是 JVM 内部的 C++ 对象。我们这里主要关注 _owner 字段和 _entryList 字段。_owner 记录了当前持有这把锁的线程,而 _entryList 则是一个等待获取这把锁的线程集合。在 Java 中,我们并不直接操作 Monitor,而是通过锁对象的Mark Word字段来找到与其关联的 Monitor

32 位 JVM 中的 Mark Word 结构 :

+---------------------------------------------------------------+
|                       Mark Word (32 bits)                       |
+---------------------------------------------------------------+
| 哈希码 (25 bits) | GC 分代年龄 (4 bits) | 偏向标志 (1 bit) |锁标志位 (2 bits)                                                             | 
+---------------------------------------------------------------+

不要被Mark Word的结构迷惑,实际上Mark Word的作用很简单:与锁对象关联的 Monitor 建立联系。但是容易发现,在 32 位 Mark Word 中并没有直接指向 Monitor 的字段啊?原理是这样的:当锁升级为重量级锁时,对象头的 Mark Word 会被替换为指向 Monitor 对象的指针(同时锁标志位变为 10)。而对象 Mark Word 中原来的信息则会备份到 Monitor 对象的 _header 字段中。

  • 总结获取重量级锁的流程:
  1. 线程尝试获取锁: 当一个线程尝试执行被 synchronized 关键字修饰的代码块或方法时,发现目标对象的锁状态已经是重量级锁,并且该锁不是由当前线程持有。
  2. 进入 Monitor 的等待队列: 该线程会被封装成一个 ObjectWaiter 对象,并直接加入到与该对象关联的 Monitor 对象的等待队列的末尾(通常是 _entryList,具体实现取决于 JVM)。
  3. 线程阻塞: 加入等待队列后,当前线程会被阻塞(挂起),不再占用 CPU 资源,进入等待被唤醒的状态。
  4. 锁的释放与唤醒: 当持有该重量级锁的线程执行完毕 synchronized 代码块或方法,或者调用了 wait() 方法释放锁时,Monitor 会从其等待队列中唤醒一个或多个等待的线程。唤醒的策略通常是先进先出(FIFO)。
  5. 竞争锁: 被唤醒的线程会尝试竞争成为新的锁持有者。这个竞争通常通过原子操作(例如 CAS)来尝试将 Monitor 对象的 _owner 字段指向自己。
  6. 获取成功: 竞争成功的线程会成为新的锁持有者,并将对象头的 Mark Word 指向该 Monitor 对象,同时更新 Monitor 内部的锁计数器(用于支持重入)。该线程可以继续执行 synchronized 代码。
  7. 获取失败: 竞争失败的线程会重新进入 Monitor 的等待队列并再次被阻塞,等待下一次被唤醒。

重量级锁synchronized 的最终形态,它依赖于操作系统的互斥量(Mutex),涉及到线程的阻塞和唤醒,因此开销相对较大。这也是 JVM 会尝试使用偏向锁和轻量级锁进行优化的原因。锁优化部分有空再补充。

你可能感兴趣的:(java,jvm)