目录

一、背景

二、Java线程模型

三、Synchronized实现原理

3.1 锁的使用

3.2 解释执行

3.3 JIT执行

3.4 锁的状态

3.5 monitorenter

3.5.1 偏向锁

3.5.2 轻量级锁

3.5.3 重量级锁

3.6 monitorexit

3.6.1 偏向锁

3.6.2 轻量级锁

3.6.3 重量级

四、可见性的真相

4.1 缓存一致性协议

4.2 根因分析

五、CAS实现原理

5.1 CAS介绍

5.2 CAS特性

5.3 CAS实现原理

六、Volatile

6.1 字节码

6.2 屏障类型

6.3 volatile修饰的写操作

6.4 volatile修饰的读操作

6.5 如何保证可见性

七、Reentrantlock实现原理

7.1 AQS

7.2 lock

7.3 unlock

7.4 CLH队列

7.5 与 synchronized 关键字的对比

7.5 可中断锁和超时锁

7.6 Condition

八、线程阻塞或唤醒时操作系统的动作

九、原子性、有序性、可见性的保证


一、背景

讲解一下在java中涉及到并发的相关基础知识,深入理解 synchronized, volatile, CAS, ReentrantLock 与内存可见性、原子性、有序性

二、Java线程模型

现代JVM(如HotSpot)默认采用此模型:每个Java线程直接绑定一个操作系统线程(内核线程)

  • 优点

    • 利用多核CPU并行执行。

    • 线程阻塞(如I/O)不影响其他线程。

  • 缺点

    • 线程创建/销毁开销大(需OS介入)。

    • 线程数量受限于OS(默认Linux约数千个)。

三、Synchronized实现原理

3.1 锁的使用

Java 使用synchronized关键字加锁,

Object lock = new Object();
synchronized (lock) {// 代码块
}

加上锁的代码会被编译成如下的字节码:

0: new           #2      // 创建 Object 对象
3: dup
4: invokespecial #1      // 调用 Object 构造方法
7: astore_1             // 存储到局部变量 lock8: aload_1              // 加载 lock 到操作数栈
9: dup
10: astore_2            // 存储锁对象副本
11: monitorenter        // 尝试获取锁
12: aload_2
13: monitorexit         // 正常释放锁
14: goto 20
17: aload_2
18: monitorexit         // 异常时释放锁 (确保锁被释放)
19: athrow
20: return

我们后面主要研究下 monitorenter  和  monitorexit 的底层原理,了解这两个字节码,我们首先要了解,它们是如何被java执行的

3.2 解释执行

“解释执行”的核心含义:

  1. 执行单元是单条字节码指令: 解释器(包括模板解释器)的基本工作单元是一条字节码指令。它逐条读取、解释(分派)并执行字节码指令。

  2. 没有预先的“完整编译”: 在执行一个 Java 方法之前,解释器不会把这个方法包含的所有字节码指令作为一个整体编译成一段完整的、连续的、优化过的本地机器码。它只为每指令准备了模板。

  3. 边“解释”(分派)边执行: 执行过程始终伴随着一个分派循环

    • 取指: 从当前方法的字节码流中读取下一条指令的操作码。

    • 分派: 根据这个操作码,查找或计算对应的机器码模板的入口地址。

    • 跳转执行: 跳转到该模板地址,执行对应的机器码片段。

    • 循环: 执行完这条指令的模板后,必须回到分派循环的开头,重复上述步骤处理下一条指令。

  4. 执行上下文依赖分派循环: 每条指令的机器码模板执行完毕后,控制权必须交还给解释器的分派循环。这个循环负责维护执行状态(如程序计数器 PC、栈指针等),并决定下一条要执行的指令是什么。模板本身通常不包含跳转到下一条指令的逻辑(除了像 goto 这种控制流指令,它们会直接修改 PC)。

3.3 JIT执行

  • JIT 编译: 当 HotSpot 发现某个方法是“热点”时,它的 JIT 编译器(如 C1, C2)会介入。

    • 执行单元: 它将整个方法(或一个热点循环)作为一个单元。

    • 过程: 读取该方法的所有相关字节码,进行复杂的静态分析优化(如寄存器分配、死代码消除、循环展开、方法内联、逃逸分析等),最终生成一段完整的、连续的、高度优化的本地机器码

    • 执行: 当再次调用这个方法时,JVM 直接跳转到这段编译好的机器码的起始地址。这段机器码自己负责执行整个方法的逻辑,包括控制流(跳转、循环、调用)。它完全绕过了解释器的分派循环。执行过程中不再有“取指-分派”的开销,并且代码是优化过的。

  • 关键区别: JIT 编译后执行的是一个完整优化后的代码块,而模板解释器执行的是一系列独立的、通过分派循环粘合起来的机器码片段

通过上面的解释,我们大概能理解,java执行时是边解释字节码边执行的,而不是直接翻译成机器码文件。

java会把 monitorenter  和  monitorexit解释成相关的机器码,JVM执行时会跳到monitorenter 的机器码的位置,进行执行

例如对于monitorenter  :

  • 当解释器执行到 monitorenter 字节码时:

    • 分派循环跳转到 monitorenter 的机器码模板入口。

    • CPU 执行模板中的指令:准备好参数(对象引用),然后执行 call 指令。

    • CPU 跳转到 InterpreterRuntime::monitorenter 函数的机器码(这个函数本身是 C++ 写的,在 JVM 启动时已经被编译成了机器码)。

    • InterpreterRuntime::monitorenter 的机器码执行复杂的锁逻辑(可能调用更底层的 ObjectMonitor::enter 等)。

    • InterpreterRuntime::monitorenter 函数执行完毕,通过 ret 指令返回到调用它的地方——也就是 monitorenter 模板中 call 指令的下一条指令。

    • monitorenter 模板中剩余的指令(如果有)执行。

    • 控制权返回到解释器的分派循环,准备执行下一条字节码。

下面来看下monitorenter和monitorexit的具体动作

3.4 锁的状态

📐 对象头组成及大小(以主流64位系统为例)

​组成部分​

​大小(字节)​

​说明​

​是否可变​

​Mark Word​

8

存储对象哈希码、锁状态、GC年龄等信息

❌ 固定大小

​Klass Pointer​

4(或8)

指向类元数据的指针(默认指针压缩)

✅ 可配置

​数组长度(可选)​

4

仅数组对象存在(存储数组长度)

❌ 固定大小

在不同锁状态下,保存内容如下所示

3.5 monitorenter

java8的锁粒度有 偏向锁、轻量级锁、重量级锁

3.5.1 偏向锁

  1. 检查对象头中的 Mark Word

    • 若可偏向(偏向模式位 1,锁标志位 01)且 线程ID指向当前线程:直接进入同步块(无CAS)。

    • 若可偏向但 线程ID不指向当前线程:触发偏向锁撤销(需全局安全点),升级为轻量级锁。

    • 若未偏向:通过 CAS 将 Mark Word 的线程ID设置为当前线程。

撤销偏向锁过程:

  1. 触发条件
    当线程A尝试获取偏向线程B的锁时(对象头Mark Word中的线程ID ≠ 当前线程ID),触发撤销。

  2. 暂停所有Java线程(STW)

    • JVM 触发 全局安全点(Safepoint),暂停所有Java线程(包括持有偏向锁的线程B和竞争线程A)。

    • 关键原因:对象头的Mark Word和持有锁线程B的栈帧状态需被原子修改,避免并发冲突。

  3. 撤销操作(由JVM在安全点执行)

    • 步骤1:检查持有偏向锁的线程B的状态:

      • 若线程B 已退出同步块(无活跃锁):

        • 直接重置对象头为 无锁状态(锁标志位 01,偏向模式 0)。

      • 若线程B 仍处于同步块中(活跃锁):

        • 将锁升级为 轻量级锁

          1. 在线程B的栈帧中生成锁记录(Lock Record),拷贝原Mark Word。

          2. 用CAS将对象头的Mark Word替换为指向该锁记录的指针(锁标志位 00)。

        • 若线程B已销毁:强制释放偏向锁。

  4. 恢复线程并升级竞争

    • 安全点结束后,所有线程恢复执行。

    • 竞争线程A重新尝试获取锁:

      • 此时对象头已是轻量级锁状态(标志位 00),线程A通过 CAS自旋 竞争轻量级锁。

3.5.2 轻量级锁

  1. 在栈帧中创建 锁记录(Lock Record),拷贝对象头的 Mark Word(称为 Displaced Mark Word)。

  2. 通过 CAS 将对象头的 Mark Word 替换为指向锁记录的指针(锁标志位 00)。

    • 成功:获得锁。

    • 失败(其他线程已占用):自旋重试;若自旋失败,升级为重量级锁。

cas操作的值如下:

  • 目标内存地址:对象头的 Mark Word

  • 预期值(Expected Value):对象原始的 Mark Word(从栈帧的锁记录中获取),对象原始的 Mark Word一般是偏向锁或者无锁状态

  • 新值(New Value):指向当前线程栈帧中锁记录的指针 + 轻量级锁标志位 00

3.5.3 重量级锁

  • 触发条件
    当线程尝试获取轻量级锁时,如果 CAS 失败(表示锁已被占用),线程会进入自旋重试状态。

    • 若自旋超过阈值仍失败(默认约 10-50 次,JVM 自适应调整)

    • 或自旋期间有第三个线程加入竞争

参考图:

  • 升级动作
    JVM 调用 inflateLock() 方法,创建 ObjectMonitor 对象(重量级锁结构),修改对象头标志位为 10

Monitor 对象对象结构:

  1. 检查对象关联的 Monitor 对象(位于对象头指向的 ObjectMonitor)。

  2. 调用 ObjectMonitor::enter()

    • 若 Owner 为 null:通过 CAS 设置 Owner 为当前线程。

    • 若 Owner 是当前线程:重入计数 _recursions++

    • 否则:线程进入 阻塞队列(cxq/EntryList,等待唤醒。

3.6 monitorexit

3.6.1 偏向锁

  1. 不修改对象头(保留偏向状态)。

  2. 仅检查线程ID是否匹配(防止错误解锁)。

3.6.2 轻量级锁

通过 CAS 将 Displaced Mark Word 写回对象头。

  • 成功:锁释放。

  • 失败:表明已升级为重量级锁,需走重量级锁释放流程

3.6.3 重量级

  1. 调用 ObjectMonitor::exit()

    • 重入计数 _recursions--

    • 若 _recursions == 0:清空 Owner,唤醒阻塞队列中的线程。

重量级锁变化举例:

四、可见性的真相

4.1 缓存一致性协议

多线程并发修改变量时会有可见性问题

虽然我们有缓存一致性协议,具体如下图,但它只能保证最终一致性,而不能保证中间过程的一致性:

4.2 根因分析

已知:

当CPU修改共享变量时:

  1. 使其他CPU的缓存行失效(I状态)

  2. 修改自己的缓存(M状态)

  3. 最终写回主内存

分析:

原因1:写缓冲(Store Buffer)

现代CPU使用写缓冲优化性能:

plaintext

CPU-A 操作流程:
1. b的新值存入写缓冲  ← 立即继续执行后续指令
2. 异步将写缓冲刷新到缓存(此时才触发MESI)

在步骤1→2的间隙中:

  • CPU-C读取b时,由于失效请求未到达,可能仍读取自己的旧缓存

  • 即使CPU-A认为"修改已完成",实际修改仍在缓冲中未提交

原因2:失效队列(Invalidation Queue)

plaintext

CPU-C 操作流程:
1. 收到b的失效请求 → 存入失效队列
2. 继续使用本地缓存(直到处理失效队列) ← 关键延迟点!
3. 后续读取才从主内存重新加载

在步骤1→3期间,CPU-C仍可能使用已失效的缓存值。

原因3:指令重排:

// 共享变量
int a = 0;
boolean flag = false; // 注意:非volatile!// 线程1(核心1执行)
a = 42;       // 语句1
flag = true;  // 语句2// 线程2(核心2执行)
while(!flag); // 语句3
System.out.println(a); // 可能输出0! 

具体如何解决可见性问题,我们后面分析

五、CAS实现原理

5.1 CAS介绍

CAS(Compare-And-Swap) 是一种基于硬件的原子操作,用于实现无锁(lock-free)并发编程。它是 Java 并发包(java.util.concurrent)中原子类(如 AtomicIntegerAtomicReference 等)的核心实现机制。

CAS 操作包含三个参数:

  1. 内存地址 V(需要更新的变量)

  2. 期望值 A(变量当前应具有的值)

  3. 新值 B(需要设置的新值)

操作逻辑

java

if (V == A) {V = B; // 更新值return true; // 成功
} else {return false; // 失败
}

整个过程由硬件(CPU 指令)保证原子性,不会被线程调度打断。

5.2 CAS特性

  1. 原子性(Atomicity)

    • 操作不可分割,要么完全执行成功,要么完全不执行。

    • 由底层 CPU 指令(如 x86 的 CMPXCHG)直接支持,无需锁。

  2. 无锁(Lock-Free)

    • 线程通过循环重试(自旋)更新数据,避免阻塞。

    • 示例代码:

      java

      public final int incrementAndGet(AtomicInteger atomicInt) {int prev, next;do {prev = atomicInt.get(); // 当前值next = prev + 1;        // 新值} while (!atomicInt.compareAndSet(prev, next)); // CAS 失败则重试return next;
      }
  3. 可见性(Visibility)

    • CAS 操作隐含 volatile 语义,确保修改对其他线程立即可见。

  4. 避免死锁

    • 无锁机制天然规避了死锁风险。

5.3 CAS实现原理

底层 CPU 硬件指令直接保证的

  1. x86/x86-64 架构

    • 指令:LOCK CMPXCHG

      • CMPXCHG(Compare and Exchange)是基础指令

      • LOCK 前缀(汇编指令前缀)强制独占内存访问,确保原子性

      • 工作流程:

        assembly

        ; 伪汇编代码
        LOCK CMPXCHG [memory], reg   ; [memory]为内存地址,reg为新值
        ; 比较 EAX(隐含寄存器)与 [memory] 的值
        ; 相等 → 将 reg 存入 [memory],并设置 ZF=1
        ; 不等 → 将 [memory] 加载到 EAX,并设置 ZF=0
  1. 总线锁定(Bus Locking)

    • LOCK 前缀使 CPU 在执行期间锁定内存总线

    • 阻止其他核心/CPU 访问同一内存区域

注意:CPU 在成功将缓存行标记为“独占”(Exclusive)状态之前,必须确保它拥有该缓存行的最新数据副本。如果它发现数据不是最新的,它就无法成功获得独占状态,操作会失败或需要重试。

  1. 缓存一致性协议(如 MESI)

    • 现代 CPU 通过缓存锁定代替总线锁定

    • 当 CPU 检测到 CMPXCHG 操作时:

      • 将缓存行标记为"独占"状态

      • 若其他核心尝试修改,会使其缓存行失效

      • 确保只有一个核心能成功修改

  2. 内存顺序模型支持

    • 指令隐含内存屏障(Memory Barrier)

    • 保证操作前后的内存可见性顺序

六、Volatile

6.1 字节码

代码示例:

public class Test {volatile int v;
}

字节码:Field access_flags: 0x0040 (ACC_VOLATILE)

对于 v = 42(volatile 写),字节码只有一条简单指令:

java

putfield #4  // 将值 42 写入字段 v(#4 是常量池索引)

关键点

  • 字节码 没有显式的屏障指令

  • JVM 通过字段的 ACC_VOLATILE 标志识别需要特殊处理

6.2 屏障类型

JVM 通过内存屏障实现 volatile 的语义,屏障类型如下:

屏障类型作用
LoadLoad确保当前读操作在后续读操作之前完成。
StoreStore确保当前写操作在后续写操作之前完成(刷新到主内存)。
LoadStore确保当前读操作在后续写操作之前完成。
StoreLoad全能屏障:确保当前写操作对所有处理器可见后才执行后续读/写操作。

volatile 读写操作的屏障插入规则:

  1. volatile 写操作

    • 写操作前:StoreStore 屏障(防止与前面的普通写重排序)。

    • 写操作后:StoreLoad 屏障(防止与后面的 volatile 读/写重排序)。

    java

    StoreStore Barrier
    v = 42;  // volatile 写
    StoreLoad Barrier
  2. volatile 读操作

    • 读操作前:LoadLoad + LoadStore 屏障(防止与后续操作重排序)。

    • 读操作后:无额外屏障(某些架构下合并到前面)。

    java

    int tmp = v;  // volatile 读
    LoadLoad Barrier + LoadStore Barrier

具体对应的CPU指令不写了,过程有点复杂,大概率了解一下吧

屏障类型较多,理解起来也有点费劲,我直接说结果,即有了这些屏障后,程序的运行结果是怎样的

6.3 volatile修饰的写操作

// 第一阶段:准备操作(普通读写)
普通操作1
普通操作2
...
普通操作N// StoreStore 屏障(隐形防线)
volatile 写 = 新值;  // 关键操作// StoreLoad 屏障(隐形防线)
后续操作1
后续操作2
...
后续操作M

1. 屏障前的保证(前置防护)

  • ✅ 所有 volatile 之前的操作都已计算完成
    (包括普通读/写、方法调用等)

  • ✅ 所有普通写操作结果全局可见
    (通过 StoreStore 屏障保证)

  • ❌ 绝不允许 volatile 之后的操作重排到前面
    (StoreLoad 屏障阻止后续任何操作前移)

2. 屏障后的保证(后置防护)

  • ✅ volatile 写本身全局可见
    (通过 StoreLoad 屏障强制刷新)

  • ❌ 绝不允许 volatile 之前的操作重排到后面
    (StoreStore 屏障阻止前面操作后移)

6.4 volatile修饰的读操作

// 前置操作区(可能被重排到此)
普通操作A
普通操作B

// 墙入口
int tmp = v;  // volatile读
// 隐形的双屏障防线
LoadLoad Barrier
LoadStore Barrier

// 后置保护区
后续操作1
后续操作2

1. 墙的入口(读操作本身)

  • ✅ 强制获取最新值
    使当前 CPU 缓存失效,从主内存加载最新值

  • ⚠️ 不限制前面操作
    允许墙前的普通操作重排到墙后(与写操作关键区别!)

6.5 作用总结

一、volatile 写操作的作用

当线程执行 volatile 写操作(如 volatileVar = 42;)时:

  1. 可见性保证

    • ✅ 确保该写操作完成后,所有线程都能立即看到这个新值。

    • 🔧 底层机制:强制刷新 CPU 写缓冲区,将新值同步到主内存,并通过缓存一致性协议(如 MESI)使其他 CPU 的缓存副本失效。

  2. 有序性保证

    • ⛔ 禁止指令重排序:

      • 禁止将 volatile 写之前的任何操作 重排序到写操作之后。

      • 禁止将 volatile 写之后的读操作 重排序到写操作之前。

    • 🔧 底层机制:插入 StoreStore + StoreLoad 内存屏障(如 x86 的 lock 指令)。


二、volatile 读操作的作用

当线程执行 volatile 读操作(如 int val = volatileVar;)时:

  1. 可见性保证

    • ✅ 确保读取到的是最新值(可能是其他线程刚写入的值)。

    • 🔧 底层机制:强制从主内存或其他 CPU 重新加载数据(跳过本地可能过期的缓存)。

  2. 有序性保证

    • ⛔ 禁止指令重排序:

      • 禁止将 volatile 读之后的任何操作 重排序到读操作之前。

      • 禁止将 volatile 读之前的写操作 重排序到读操作之后。

    • 🔧 底层机制:插入 LoadLoad + LoadStore 内存屏障(如 ARM 的 dmb 指令)。

6.6 x86 CPU实现

以下是几种常见架构上,为了实现 volatile 语义,HotSpot JVM 通常使用的屏障和对应的 CPU 指令:

    1. x86/x86-64:

      • volatile 写:

        • 屏障要求: StoreStore 屏障 + StoreLoad 屏障

        • 实际指令: lock addl $0x0, (%rsp) (或其他类似指令)

          • lock 前缀:这是 x86 上实现强内存屏障效果的关键。它锁定内存总线(或使用缓存一致性协议如 MESI),确保该指令的操作具有原子性,并隐式包含了 StoreLoad 屏障。它还会刷新写缓冲区。

          • addl $0x0, (%rsp):这是一个对栈顶指针 %rsp 指向的内存地址加 0 的空操作。它本身不改变数据,目的是提供一个让 lock 前缀作用的目标指令。

          • 为什么不需要显式 StoreStore? x86 架构的 TSO (Total Store Order) 内存模型本身保证了写操作(包括非 volatile 写)不会重排序。因此,在 volatile 写之前插入的 StoreStore 屏障在 x86 上通常是 空操作nop)。

      • volatile 读:

        • 屏障要求: LoadLoad 屏障 + LoadStore 屏障

        • 实际指令: 通常没有显式的屏障指令

          • 原因: x86 的 TSO 模型天然保证了:

            • LoadLoad 不会重排序(后面的读能看到前面的读的结果)。

            • LoadStore 不会重排序(后面的写不会重排序到前面的读之前)。

          • 因此,对于 volatile 读,JVM 在 x86 上通常只需要生成普通的 mov 指令来加载值,而不需要插入任何显式的屏障指令volatile 读本身的内存语义(如缓存行失效)由 CPU 的缓存一致性协议(MESI)自动处理。

      • 总结 (x86):

        • volatile 写:lock addl $0x0, (%rsp) (或等效指令) -> 主要提供 StoreLoad 屏障

        • volatile 读:普通的 mov 指令 -> 屏障是空操作

Q:那lock addl $0x0, (%rsp) 指令和 修改 volatile值的mov指令 不是原子的,岂不是会造成值修改了,但是不可见的情况?

lock addl $0x0, (%rsp) 的核心作用是:

  1. 排空写缓冲区
    强制当前 CPU 的写缓冲区中所有数据(包括之前的 mov立即刷到缓存/内存

  2. 使其他 CPU 的缓存失效
    通过缓存一致性协议(MESI)广播,使其他 CPU 中该 volatile 变量的缓存行失效。

  3. StoreLoad 屏障
    确保后续读操作必须重新从主存加载最新值。

假设 mov 执行后、lock 执行前发生中断:

x86asm

mov [var], eax     ; 值进入写缓冲区
; <-- 此处发生中断(写缓冲区未刷新)
lock addl $0x0, (%rsp) ; 中断返回后执行
  • 问题:其他 CPU 在中断期间可能读到旧值。

  • 解答:x86 的中断处理机制保证:

    1. 中断返回前会隐式排空写缓冲区(类似 sfence)。

    2. 中断结束后执行 lock 指令,再次强制刷新并失效化缓存。

  • 结果:最终仍保证可见性(可能略有延迟,但符合 Java 语义)。

七、Reentrantlock实现原理

7.1 AQS

Reentrantlock基于AQS实现,AQS介绍

  1. 状态变量 (state):

    • 一个 volatile int 类型的变量,表示锁的状态。

    • 对于 ReentrantLock

      • state = 0: 锁未被任何线程持有。

      • state > 0: 锁已被某个线程持有。数值表示该线程重入锁的次数(同一个线程多次获取锁)。

  2. CLH 队列 (FIFO 线程等待队列):

    • 一个双向链表(或变体)实现的等待队列。

    • 当多个线程竞争锁失败时,它们会被构造成 Node 节点,并加入到这个队列尾部排队等待。

    • 队列中的线程会以 FIFO(先进先出)的顺序被唤醒(公平模式下严格 FIFO,非公平模式下可能插队)。

7.2 lock

    • 尝试获取 (tryAcquire):

      • 线程首先尝试通过 CAS 操作将 state 从 0 改为 1。

      • 成功 (state 原来是 0):

        • 设置当前线程为锁的独占所有者 (exclusiveOwnerThread)。

        • 返回 true,获取锁成功。

      • 失败 (state > 0):

        • 检查当前线程是否已经是锁的持有者 (exclusiveOwnerThread == currentThread)。

        • 如果是,则将 state 加 1(重入计数增加),返回 true。

        • 如果不是,返回 false。

    • 加入队列等待 (acquireQueued):

      • 如果 tryAcquire 失败(返回 false),线程会将自己包装成一个 Node 节点。

      • 使用 CAS 操作将节点安全地加入到 CLH 队列的尾部。

      • 进入自旋或阻塞状态:

        • 检查自己是否是队列中第一个有效等待节点(头节点的后继)。

        • 如果是,再次尝试 tryAcquire (非公平锁总是尝试一次;公平锁严格排队)。

        • 如果还不是第一个节点或尝试失败:

          • 检查前驱节点的状态,判断是否需要阻塞自己。

          • 调用 LockSupport.park(this) 将当前线程挂起(阻塞)。

        • 线程被唤醒(通常是前驱节点释放锁时唤醒它)后,会再次尝试获取锁。

7.3 unlock

  • 尝试释放 (tryRelease):

    • 检查当前线程是否是锁的持有者(exclusiveOwnerThread == currentThread),否则抛出 IllegalMonitorStateException

    • 将 state 减 1(表示减少一次重入计数)。

    • 如果 state 减到 0:

      • 将 exclusiveOwnerThread 设置为 null,表示锁完全释放。

      • 返回 true。

    • 如果 state 仍然大于 0(说明还有重入),返回 false。

  • 唤醒后继 (unparkSuccessor):

    • 如果 tryRelease 返回 true(锁完全释放),则找到 CLH 队列中第一个状态正常的等待节点(通常是头节点的后继)。

    • 调用 LockSupport.unpark(s.thread) 唤醒该节点对应的线程,使其有机会去竞争锁。

相关代码:

7.4 CLH队列

前面加锁和解锁过程都使用了到了CLH队列,下面具体介绍一下什么是CLH队列,以及在Reentrantlock中做了哪些优化

  • 核心思想: 一个隐式链接的 FIFO 队列,用于管理等待锁(或共享资源)的线程。每个等待线程被封装成一个节点(Node)

  • 关键机制: 每个节点通过一个 volatile 状态字段(通常是 waitStatus 来轮询(spin) 其前驱节点(predecessor node) 的状态。

  • 核心操作:

    • 入队(获取锁失败时): 新线程尝试获取锁失败后,会创建一个新节点,通过 CAS (Compare-And-Swap) 操作将自身设置为新的 tail(尾指针),同时记录下它入队时看到的尾节点作为其前驱节点(prev)

    • 等待(自旋轮询前驱): 线程在一个循环中不断检查其前驱节点的状态标志(waitStatus)。如果前驱节点的状态表明它已经释放了锁(或即将释放),那么当前线程就有资格尝试获取锁。关键点:线程只关心它前面的那个节点(前驱)的状态。

    • 出队(获取锁成功): 当线程检测到前驱节点释放了锁(状态变为 SIGNAL 或类似),它成功获取锁,并成为新的队列头(head)。原头节点通常被移除或成为虚拟头节点。

    • 释放锁(通知后继): 当持有锁的线程(队列头节点)释放锁时,它会检查其后继节点(next)的状态。如果后继节点在等待(状态为 SIGNAL),它会修改自身的状态(例如,设置 waitStatus = 0 或直接清除状态)或直接设置后继节点的状态(在 AQS 变体中通常是设置头节点状态为 SIGNAL 后由后继轮询),这个状态变化会被其后继节点(正在轮询前驱状态)立即感知volatile 保证可见性),从而唤醒后继节点去尝试获取锁。

  • 原始 CLH 特点(纯自旋):

    • FIFO 公平性。

    • 线程在 CPU 上忙等(自旋)其前驱的状态变化。

    • 锁释放仅需修改一个 volatile 变量(自身状态),通知是无锁且高效的。

    • 避免了“惊群效应”(只唤醒一个后继)。

AQS中如何使用?

  1. 显式双向链表:

    • AQS 节点不仅维护指向前驱节点(prev 的指针(用于轮询状态和取消时移除),还维护指向后继节点(next 的指针。这不是原始 CLH 必需的(原始 CLH 通常只有隐式前驱链),但大大简化了节点取消(Cancellation)(中断、超时)时的链表操作。

  2. 虚拟头节点(Dummy Head):

    • AQS 队列初始化时通常会创建一个不关联任何线程的虚拟头节点。第一个真正等待的线程节点会成为虚拟头节点的后继。这使得队列操作(如判断是否有等待线程、唤醒后继)逻辑更统一,避免边界条件判断。

  3. 状态整合(waitStatus):

    • AQS 节点的 waitStatus 字段承载了比原始 CLH 状态标志更丰富的含义:

      • SIGNAL(-1)最重要的状态。 表示该节点的后继节点需要被唤醒(即,后继节点在等待)。当前节点释放锁或取消时,必须唤醒其后继节点。节点在阻塞自己之前,通常会将其前驱节点的 waitStatus 设置为 SIGNAL(通过 CAS),这样前驱节点就知道“我后面有人等着呢,你释放时记得叫我”。

      • CANCELLED(1): 节点关联的线程已取消等待(如超时或中断)。需要从队列中安全移除。

      • CONDITION(-2): 节点当前在条件队列ConditionObject)中等待,而不是在主同步队列中。

      • PROPAGATE(-3)仅用于共享模式。 表示下一次 acquireShared 操作应该无条件传播(表示后续节点也可能可以获取共享资源)。

      • 0: 初始状态,或表示节点不处于上述任何特殊状态。

  4. 阻塞代替自旋(关键优化):

    • 这是 AQS CLH 变体最核心的改进! 原始 CLH 是纯自旋(忙等),消耗 CPU。

    • AQS 中,线程不会持续自旋轮询前驱状态。其流程是:

      • a) 尝试获取锁。

      • b) 失败,创建节点并入队(设置 tail,链接 prev)。

      • c) 快速自旋检查前驱状态: 在一个循环中快速检查几次:

        • 如果前驱是头节点(说明快轮到自己了),再次尝试获取锁(防止不必要的阻塞)。

        • 如果前驱节点的 waitStatus == SIGNAL,说明前驱已设置好“释放时唤醒我”的标志,安全地阻塞自己(调用 LockSupport.park())。

        • 如果前驱状态是 CANCELLED,则跳过该前驱,继续找更前面的有效前驱。

        • 如果前驱状态正常但不是 SIGNAL,则尝试用 CAS 将前驱的 waitStatus 设置为 SIGNAL(告诉它“你释放时记得叫我”)。

      • d) 如果步骤 c 中的检查表明可以安全阻塞了(前驱是 SIGNAL),则调用 park() 挂起当前线程。

    • 唤醒: 当持有锁的线程(头节点)释放锁时,它会检查头节点的 waitStatus。如果是 SIGNAL(通常都会是,因为后继在阻塞前设置了它),它会找到其后继节点(next),并调用 LockSupport.unpark(successor.thread) 唤醒其后继线程。被唤醒的线程从 park() 处返回,回到步骤 c 的循环中,再次尝试获取锁。

  5. 入队(enq)与设置前驱状态(shouldParkAfterFailedAcquire):

    • 入队操作 (enq) 使用 CAS 保证线程安全地更新 tail

    • shouldParkAfterFailedAcquire 方法实现了步骤 c 中的逻辑:检查/清理前驱状态,确保前驱是有效的且 waitStatus == SIGNAL,然后才决定调用 park()

7.5 与 synchronized 关键字的对比

  • 相似点:都是可重入互斥锁。

  • ReentrantLock 优势

    • 灵活性:支持公平锁/非公平锁选择、可中断锁等待 (lockInterruptibly)、超时锁等待 (tryLock(timeout))、多条件变量 (newCondition)。

    • API 化:显式的 lock() 和 unlock() 操作,控制更精细。

    • 性能:在高度竞争的场景下,现代 JVM 的 synchronized 优化(锁升级)已经非常高效,两者性能差距不大。但在某些特定场景(如大量读少量写),ReentrantLock 结合 ReadWriteLock 可能更优。

  • synchronized 优势

    • 简洁性:语法简单,由 JVM 自动管理锁的获取和释放(在 synchronized 代码块结束时释放),不易出错(避免忘记 unlock)。

    • JVM 优化:JVM 深度优化(偏向锁、轻量级锁、锁消除、锁粗化等)。

下面说一下reentrantlock实现的

7.5 可中断锁和超时锁

可中断锁:

源码:

private void doAcquireInterruptibly(int arg) throws InterruptedException {final Node node = addWaiter(Node.EXCLUSIVE); // 将当前线程包装为独占模式Node加入队列尾部boolean failed = true;try {for (;;) { // 自旋等待final Node p = node.predecessor(); // 获取前驱节点if (p == head && tryAcquire(arg)) { // 如果前驱是头节点(轮到我了)且尝试获取锁成功setHead(node); // 将自己设为新的头节点(出队)p.next = null; // help GCfailed = false;return; // 成功获取锁,退出方法}if (shouldParkAfterFailedAcquire(p, node) && // 检查是否应该阻塞(前驱状态正常)parkAndCheckInterrupt()) // 4. 真正阻塞线程,并在此处检查中断!throw new InterruptedException(); // 5. 如果阻塞中被中断,抛出异常!}} finally {if (failed)cancelAcquire(node); // 6. 如果最终失败(如因中断),取消节点获取状态}
}private final boolean parkAndCheckInterrupt() {LockSupport.park(this); // 使用LockSupport.park()挂起当前线程return Thread.interrupted(); // 线程被唤醒后,立即检查并清除中断标志,返回是否因中断被唤醒
}

当线程判断需要阻塞时(通过 shouldParkAfterFailedAcquire),调用 LockSupport.park(this) 挂起当前线程

  • 线程可能被以下三种方式唤醒:

    1. 持有锁的线程释放锁,并唤醒队列中的后继节点(unparkSuccessor)。

    2. 其他线程调用了该线程的 interrupt() 方法。

    3. 虚假唤醒(spurious wakeup)。

  • 线程被唤醒后,第一件事就是调用 Thread.interrupted()。这个方法做两件事:

    • 返回线程当前的中断状态(true 表示被中断过)。

    • 清除线程的中断状态(设为 false)。

超时锁:

final boolean doAcquireNanos(int arg, long nanosTimeout) {if (nanosTimeout <= 0L) return false; // 时间已到,直接失败final long deadline = System.nanoTime() + nanosTimeout;final Node node = addWaiter(Node.EXCLUSIVE); // 加入队列boolean failed = true;try {for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) { // 如果是队首则尝试获取锁setHead(node);p.next = null; // 帮助GCfailed = false;return true;}nanosTimeout = deadline - System.nanoTime();if (nanosTimeout <= 0L) return false; // 超时检查if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) { // 大于阈值才挂起LockSupport.parkNanos(this, nanosTimeout); // 关键:挂起指定时间}if (Thread.interrupted()) // 响应中断throw new InterruptedException();}} finally {if (failed) cancelAcquire(node); // 失败则取消节点}
}

其实可以看到 无论是可中断锁还是超时锁,它们都使用了LockSupport这个对象来加锁解锁,没有采用synchronized这种操作,而LockSupport支持锁中断,支持锁超时机制,所以这就是reetrantlock能实现这些多功能锁的原因了

7.6 Condition

基本使用:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();// 等待方
lock.lock();
try {while (!conditionSatisfied) {  // 循环检查条件condition.await();         // 释放锁并等待}// 执行条件满足后的操作
} finally {lock.unlock();
}// 通知方
lock.lock();
try {// 改变条件condition.signal();  // 或 signalAll()
} finally {lock.unlock();
}

实现原理简要介绍:

Condition 的核心实现基于 AQS(AbstractQueuedSynchronizer),讲解Condition时会涉及两个队列,一个是同步队列,就是AQS存储线程的队列,一个是条件队列,指的是condition对象对应的队列,以下是关键原理:

  1. 等待队列(条件队列)

    • 每个 Condition 对象内部维护一个 FIFO 等待队列(单向链表)。

    • 当线程调用 await() 时,会释放锁、修改对应lock锁的状态、并进入条件队列等待。

    • 队列节点类型与 AQS 同步队列相同(Node 类)。

  2. 节点转移机制

    • 调用 signal() 时,将条件队列的头节点移动到 AQS 同步队列尾部。正好对应上了上面代码的isOnSyncQueue

    • 移动到同步队列的节点会尝试获取锁,成功后从 await() 返回。

  3. 唤醒与阻塞控制

    • await():释放锁 → 阻塞线程 → 加入条件队列。

    • signal():将条件队列的首节点移入同步队列,唤醒线程。

    • signalAll():移动条件队列所有节点到同步队列。

  4. 避免虚假唤醒
    通过循环检查条件(while (condition) await())确保线程被唤醒时条件真正满足。

八、线程阻塞或唤醒时操作系统的动作

1. 用户态尝试获取锁

  • 步骤

    1. 线程在用户态通过 CAS(Compare-And-Swap)自旋尝试获取锁(例如 synchronized 的偏向锁/轻量级锁)。

    2. 若锁未被竞争(无冲突),直接获取成功,全程在用户态完成

  • 关键点
    此时无系统调用,不涉及内核态切换,性能极高。

2. 竞争失败:进入自适应自旋

  • 步骤

    1. 当 CAS 失败(锁被其他线程占用),JVM 启动自适应自旋(根据历史成功率动态调整自旋次数)。

    2. 线程在用户态循环重试,仍不切内核态

  • 关键点
    自旋避免了立即陷入内核,但消耗 CPU 时间。

3. 自旋失败:真正阻塞(切内核态)

  • 步骤

    1. 自旋仍无法获取锁时,JVM 调用底层操作系统的线程阻塞原语(如 Linux 的 futex())。

    2. 线程状态从 RUNNABLE 变为 BLOCKED

    3. 触发系统调用(如 futex)→ 从用户态陷入内核态

  • 关键点
    此处是用户态切内核态的核心节点!

4. 内核态操作

  • 内核完成以下动作

    1. 保存线程上下文(寄存器、程序计数器等)。

    2. 将线程移入锁的等待队列(由内核管理)。

    3. 触发线程调度:从就绪队列选择新线程运行。

    4. 切换 CPU 上下文到新线程。

  • 关键点
    所有操作均在内核态完成,需要特权指令。

5. 唤醒阶段(再次切内核态)

  • 当锁释放时

    1. 持有锁的线程调用 unlock(),触发 JVM 的唤醒机制。

    2. JVM 通过 futex_wake() 系统调用陷入内核态

    3. 内核从等待队列中移出一个/多个线程,标记为就绪状态。

    4. 线程重新参与调度,下次被 CPU 选中时恢复执行。

  • 关键点
    唤醒操作同样需切内核态,恢复线程时需切换回用户态。

一点操作系统知识,大概了解一下:

上面使用到了futex,操作系统将所有等待同一 futex(相同 uaddr)的线程被组织在同一个等待队列中

1. uaddr 的本质

  • uaddr 是用户空间的一个内存地址(32位整数),通常指向:

    • 锁状态变量(如 Java 对象头中的 MarkWord)

    • 条件变量标志位

    • 信号量计数器

  • 关键特性
    相同 uaddr 值 → 代表同一同步资源(如同一把锁)

  • 一对一映射
    每个唯一的 uaddr 值对应 一个且仅一个 内核等待队列

  • 自动创建
    当首个线程在某个 uaddr 上调用 futex_wait() 时,内核自动创建队列

  • 自动销毁
    当队列为空(无等待线程)时,内核自动销毁队列

// 每个 futex 等待队列
struct futex_queue {struct list_head threads; // 线程链表(FIFO)u32 *uaddr;               // 绑定的用户态地址atomic_t waiters;         // 等待线程计数
};

九、原子性、有序性、可见性的保证

特性保障方法
原子性synchronizedReentrantLock、原子类(AtomicInteger 等)、CAS
可见性volatilesynchronized/锁、final、CAS
有序性volatile(禁止重排序)、synchronized/锁(内存屏障)、fina、CAS

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/918323.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/918323.shtml
英文地址,请注明出处:http://en.pswp.cn/news/918323.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

线程池111

线程池框图C语言线程池详解&#xff1a;从基础到实现通俗理解线程池想象你开了一家快递站&#xff0c;每天要处理很多包裹派送&#xff1a;​没有线程池​&#xff1a;每来一个包裹就雇一个新快递员&#xff0c;送完就解雇问题&#xff1a;频繁招聘解雇成本高&#xff08;线程创…

Qt-Advanced-Docking-System

直译一下 &#xff1a; 先进的停靠系统 github: mfreiholz/Qt-Advanced-Docking-System: Advanced Docking System for Qt 这是这个项目的起源 这个最后一次更新&#xff1a; githubuser0xFFFF/Qt-Advanced-Docking-System: Advanced Docking System for Qt 这是另一个人复刻…

湖南(源点咨询)市场调研 如何在行业研究中快速有效介入 中篇

我们接着起头篇来说迈克尔波特认为一个行业内存在着五种基本竞争力量&#xff0c;即潜在入侵者、替代产品、供方、需方以及行业内现有竞争者。如附图&#xff1a;即&#xff1a;同行业内现有竞争者的竞争能力、潜在竞争者进入的能力、替代品的替代能力、供应商的讨价还价能力、…

【无标题】消息队列(Message Queue)是一种**进程间通信(IPC)机制

消息队列&#xff08;Message Queue&#xff09;是一种进程间通信&#xff08;IPC&#xff09;机制&#xff0c;它允许进程通过在队列中添加和读取消息来交换数据。与管道&#xff08;命名/匿名&#xff09;相比&#xff0c;消息队列具有结构化消息、异步通信和消息持久化等特点…

mac中多版本JDK配置和切换

下载 从jdk官网下载即可&#xff0c;找到自己要用的版本。 官网&#xff1a;https://www.oracle.com/java/technologies/downloads/#jdk21-mac 我这里下载的jdk1.8和21。 根据自己芯片下载&#xff0c;一般都是m芯片。下载好后&#xff0c;点击&#xff0c;一直下一步就行&…

【JVM】流程汇总

【JVM】流程汇总【一】编译过程和内存分布【1】案例程序&#xff1a;简单的 Java 类【2】Java 编译过程&#xff1a;从.java到.class&#xff08;1&#xff09;编译命令&#xff08;2&#xff09;编译结果&#xff08;3&#xff09;字节码的作用【3】Java 运行过程&#xff1a;…

专业MP3瘦身工具WinMP3Shrink 1.1,绿色单文件,极速压缩

[软件名称]: 专业MP3瘦身工具WinMP3Shrink 1.1 [软件大小]: 1.1 MB [软件大小]: 夸克网盘 | 百度网盘 软件介绍 WinMP3Shrink 是一款免费的 MP3 压缩软件&#xff0c;能够有效减少 MP3 文件的体积&#xff0c;同时还能增强音质。即使不重新编码&#xff0c;通过移除保留空间…

LeetCode 每日一题 2025/8/4-2025/8/10

记录了初步解题思路 以及本地实现代码&#xff1b;并不一定为最优 也希望大家能一起探讨 一起进步 目录8/4 904. 水果成篮8/5 3477. 水果成篮 II8/6 3479. 水果成篮 III8/7 3363. 最多可收集的水果数目8/8 808. 分汤8/9 231. 2 的幂8/10 869. 重新排序得到 2 的幂8/4 904. 水果…

Python爬虫实战:研究Ruia框架,构建博客园文章采集系统

1. 引言 1.1 研究背景与意义 在数字化时代,数据已成为驱动科技创新与产业升级的核心生产要素。互联网作为全球最大的信息载体,蕴含着亿级结构化、半结构化与非结构化数据,这些数据在商业决策、学术研究、公共服务等领域具有不可替代的价值。网络爬虫技术作为自动获取网络公…

Office安装使用?借助Ohook开源工具?【图文详解】微软Office产品

一、问题背景 很多用户在使用 Office 软件一段时间后&#xff0c;会遇到以下问题。 二、解决方案 Ohook 是 Office 独有的可用方式&#xff0c;源自 GitHub 上的开源项目&#xff0c;代码开源&#xff08;开源地址&#xff1a;https://github.com/asdcorp/ohook&#xff09;。 …

LeetCode简单题 - 学习

力扣题库 - 简单题 - 仅记录学习 来源地址&#xff1a; 力扣 (LeetCode) 全球极客挚爱的技术成长平台 1. 两数之和 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你…

Android Camera 打开和拍照APK源码

完整下载路径: 【免费】AndroidcameraAPK完整源码(包括打开摄像头和拍照保存功能)Android10验证可完整运行资源-CSDN下载 效果: 源码: package com.example.mycamera;import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appco…

【系统分析师】软件需求工程——第11章学习笔记(上)

软件需求工程是包括创建和维护软件需求文档所必需的一切活动的过程。可分为两大工作&#xff1a;需求开发需求获取需求分析需求定义&#xff08;编写需求规格说明书&#xff09;需求验证需求管理定义需求基线处理需求变更需求跟踪在需求开发阶段需要确定软件所期望的用户类型&a…

机器学习第七课之支持向量机SVM

目录 简介&#xff1a; 一、什么是支持向量机 二、如何选取最佳的超平面 1.超平面方程 (优化目标) 2.如何寻找最优的超平面 3.举例分析 4.软间隔​编辑 三、核函数 1举例 2常用核函数 3.多项式核函数 4.高斯核函数: 四、svm的优缺点 五、支持向量机的API 六、案例…

P3232 [HNOI2013] 游走,solution

原题&#xff1a; link&#xff0c;点击这里喵。 题意&#xff1a; 给定一个 nnn 个点 mmm 条边的无向连通图&#xff0c;图无重边和自环&#xff0c;顶点从 111 编号到 nnn&#xff0c;边从 111 编号到 mmm。 小 Z 在该图上进行随机游走&#xff0c;初始时小 Z 在 111 号顶…

Docker容器部署discuz论坛与线上商城

准备 关闭防火墙&#xff0c;上下文[rootdocker ~]# systemctl disable --now firewalld[rootdocker ~]# setenforce 0下载应用yum remove runc -y ### rocky8才需要yum install -y yum-utils yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/cento…

Linux入门指南:26个基础命令全解析

目录 一.基础概念与入门 1.Linux操作系统简介 2.终端与shell的基本概念 3.命令行界面的优势 二.基础指令 1.whoami ​2.useradd/userdel/passwd ​3.pwd ​4.ls ​5.cd 6.touch 7.mkdir 8.tree 9.rmdir/rm 10.man 11.cp 12.mv 13.cat 14.le…

【后端】Java 8 特性 `User::getId` 语法(方法引用)介绍

文章目录核心概念解析&#xff1a;方法引用的四种类型&#xff1a;关键特性&#xff1a;使用场景推荐&#xff1a;何时避免使用&#xff1a;性能说明&#xff1a;在 Java 中&#xff0c; User::getId 是一种称为 方法引用&#xff08;Method Reference&#xff09; 的语法糖&a…

基于BP与CNN的图像分类模型构建、超参数优化及性能对比研究​

一、实验目的实验目标构建基于神经网络模型的数据分析与模式识别框架&#xff0c;探明神经网络在大数据分析中的意义。实验任务构建基于深度 BP 神经网络与卷积神经网络的数据分析与模式识别框架&#xff0c;将数据集 MNIST 与 CIFAR-10 分别在两种模型中训练&#xff0c;并比较…

HarmonyOS应用开发-低代码开发登录页面(超详细)

本篇文章我来手把手教大家做一个HarmonyOS 应用的登录页面&#xff0c;逐步讲解&#xff0c;非常细致&#xff0c;百分百能学会&#xff0c;并提供全部源码。页面使用 DevEco Studio 的低代码开发。 通过本文的实践经验&#xff0c;我想告诉大家&#xff0c; HarmonyOS 应用开发…