从OS层面深入剖析JVM如何实现多线程与同步互斥

一、深入剖析Mark Word

1. Mark Word中的字段总揽

  • Mark Word 存储的"概念字段"(按用途分类,不按位数):

    1. 对象的 身份哈希值(identityHashCode)(若已计算并缓存)

    2. 对象的 GC 年龄(age)(用于分代晋升决策)

    3. 锁/线程相关信息(包括偏向锁的线程 id / epoch、轻量级锁所需的指针/指向栈上 LockRecord 的指针、或膨胀后的重量级监视器(Monitor)指针)

    4. 标志位/状态位(标识当前 mark word 存在的状态:未锁定、有偏向锁、处于轻量级锁、处于重量级锁、在 GC 时作为转发指针/标记等)

    5. 在移动式 GC(copying)期间 可能被临时替换为 forwarding pointer(指向新地址) 或 GC 标记信息。

  • 对象最小占用内存(常见)

    • 64-bit HotSpot(开启 compressed oops & compressed klassptr 的典型现代配置) :对象头通常为 12 bytes (8 字节 mark word + 4 字节 klass pointer),由于 HotSpot 默认对象对齐为 8 bytes,最小对象大小 = 16 bytes

    • 64-bit(不使用 compressed klassptr) :头部 16 bytes(8 + 8),最小对象大小通常仍为 16 bytes(头部已是对齐倍数)。

    • 32-bit HotSpot :头部通常为 8 bytes (4 + 4),对象对齐通常是 8 bytes,所以最小对象大小 = 8 bytes (头部本身即占满)。

      以上是常见情况;具体值还可由 JVM 参数(如 -XX:ObjectAlignmentInBytes-XX:+UseCompressedOops 等)与平台决定。

对象头在不同架构下的组成:

复制代码
// 对象头在不同场景下的完整结构
+-----------------------------------------------------------------------+
|                        对象头 (Object Header)                          |
| +----------------------+----------------------+----------------------+ |
| |     Mark Word        |   Klass Pointer      |   数组长度 (可选)     | |
| | (运行时数据, 64/32位) | (类型指针, 32/64位)  | (数组对象, 32位)      | |
| +----------------------+----------------------+----------------------+ |
+-----------------------------------------------------------------------+
|                     实例数据 (Instance Data)                           |
+-----------------------------------------------------------------------+
|                      对齐填充 (Padding)                                |
+-----------------------------------------------------------------------+

// 64位系统下的具体字节分布
+------------------------------------------------------------+
|                    64位 JVM 对象头布局                      |
| +-------------------------------------------+-------------+ |
| |              Mark Word (8字节)            | Klass Ptr   | |
| |                                           | (4/8字节)   | |
| +-------------------------------------------+-------------+ |
| |       数组长度 (4字节,仅数组对象)         | 对齐填充     | |
| +-------------------------------------------+-------------+ |
+------------------------------------------------------------+

// 具体配置下的字节大小
配置场景                     Mark Word   Klass Pointer   数组长度      总对象头
-------------------------------------------------------------------------------
32位JVM                      4字节         4字节          4字节       12字节
64位JVM,压缩指针关闭        8字节         8字节          4字节       24字节
64位JVM,压缩指针开启        8字节         4字节          4字节       20字节
64位JVM,普通对象(非数组)    8字节         4字节          无          12字节

Mark Word 完整结构:

复制代码
// Mark Word 在不同锁状态下的位布局(64位系统)
public class MarkWordLayout {
    
    // ============== 无锁状态 (Normal) ==============
    // 布局:unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2
    class NormalMarkWord {
        // 具体位分布(从高位到低位,总共64位):
        // [63-39] 25位:未使用
        // [38-8]  31位:identity_hashcode(对象哈希码)
        // [7]     1位:未使用
        // [6-3]   4位:分代年龄(age,0-15)
        // [2]     1位:偏向锁标志(biased_lock,0表示无偏向)
        // [1-0]   2位:锁标志位(lock,01表示无锁/偏向锁)
        
        // 示例值:0x0000000000000001
        // lock: 01, biased_lock: 0, age: 0, hashcode未计算
    }
    
    // ============== 偏向锁状态 (Biased) ==============
    // 布局:thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2
    class BiasedMarkWord {
        // 位分布:
        // [63-10] 54位:JavaThread*(偏向线程指针)
        // [9-8]   2位:epoch(偏向时间戳,解决批量重偏向)
        // [7]     1位:未使用
        // [6-3]   4位:分代年龄
        // [2]     1位:偏向锁标志(1表示启用偏向)
        // [1-0]   2位:锁标志位(01)
        
        // 示例值:0x00007f9b00000505
        // 线程指针:0x00007f9b0000,epoch: 1,age: 0,biased_lock: 1,lock: 01
    }
    
    // ============== 轻量级锁状态 (Lightweight Locked) ==============
    // 布局:ptr_to_lock_record:62 | lock:2
    class LightweightLockedMarkWord {
        // 位分布:
        // [63-2] 62位:指向栈中锁记录的指针
        // [1-0]  2位:锁标志位(00)
        
        // 说明:轻量级锁时,对象头的Mark Word被复制到线程栈的锁记录中,
        // 对象头指向这个锁记录
    }
    
    // ============== 重量级锁状态 (Heavyweight Locked) ==============
    // 布局:ptr_to_monitor:62 | lock:2
    class HeavyweightLockedMarkWord {
        // 位分布:
        // [63-2] 62位:指向ObjectMonitor的指针
        // [1-0]  2位:锁标志位(10)
        
        // 说明:重量级锁时,对象关联一个ObjectMonitor对象
    }
    
    // ============== GC标记状态 (Marked for GC) ==============
    // 布局:空(由GC算法使用):62 | lock:2
    class GCMarkedWord {
        // 位分布:
        // [63-2] 62位:GC标记信息(不同GC算法使用方式不同)
        // [1-0]  2位:锁标志位(11)
    }
}

Klass Pointer 的作用与结构:

复制代码
class KlassPointerAnalysis {
    
    // Klass Pointer指向的是Klass对象,而非Class对象
    // 内存布局关系:
    // 对象头 -> Klass Pointer -> Klass元数据 -> Java类Class对象
    
    // Klass结构(简化)
    class Klass {
        // 1. 继承体系信息
        Klass* _super;           // 父类Klass
        Klass* _subklass;        // 第一个子类
        Klass* _next_sibling;    // 下一个兄弟类
        
        // 2. 类元数据
        ConstantPool* _constants; // 常量池
        Array<Method*>* _methods; // 方法数组
        Array<Klass*>* _inner_classes; // 内部类
        
        // 3. 实例布局信息
        int _layout_helper;      // 布局帮助器
        int _nonstatic_field_size; // 非静态字段大小
        int _static_field_size;  // 静态字段大小
        
        // 4. 访问标志
        AccessFlags _access_flags; // public, final等
        
        // 5. Java镜像
        oop _java_mirror;        // 对应的java.lang.Class对象
    }
    
    // 压缩指针下的Klass Pointer
    class CompressedKlassPointer {
        // 32位压缩指针寻址公式:
        // 实际地址 = 压缩指针 << 3 + 堆基址
        
        // 示例:
        // 压缩指针值:0x12345678
        // 堆基址:0x0000000100000000
        // 实际Klass地址:0x12345678 << 3 + 0x0000000100000000
        //              = 0x000000011A2B3C00
        
        // 最大寻址范围:32GB(2^32 * 8字节)
    }
}

2. Mark Word 能承载的"角色"与原因

对象头是每个对象必有的固定元数据区。将与对象实例相关的锁状态、hash、年龄、GC 临时信息打包在一起的好处是:

  • 访问迅速(对象引用指到对象内,头部紧邻)

  • 同时只需少量原子操作(CAS)即可完成锁状态切换 / 记录哈希 / 暂存 GC forwarding pointer

  • 节省内存(与把这些信息放在单独结构相比更紧凑)

因此 Mark Word 被设计为"可变解释的位域(tagged word)"------不同状态下其位域含义不同(即"同一块内存按当前状态解释")。


3. Mark Word 典型"字段/语义"清单

注意:下面是语义层面的字段列表,实际位偏移与位宽在不同 JVM/版本/平台会有差别。我会在每个条目后说明常见的使用场景与转换。

  1. Lock/state flag(状态标志位)

    • 表示当前 mark word 正在以哪种"解释模式"存在:如 0 = unlockedbiasedlightweightmonitormarked-for-GC/forward 等。

    • 用途:每次对对象加/解锁或 GC 发生时,首先读这些标志,决定如何解释其余位。

  2. 偏向线程 id / epoch(biased lock)

    • 当启用偏向锁并且对象被某线程"偏向"(optimistically 偏向到第一个锁它的线程)时,mark word 中会存入该线程的 id(或线程指纹)及一个 epoch/age 字段。

    • 用途场景 :无争用的 synchronized(obj),首次加锁的成本极低(只需原子操作设置 mark word),后续同一线程再次进入无需 CAS;在出现其他线程竞争时 VM 需要撤销偏向(偏向撤销会执行一些全局或局部操作)。

  3. 轻量级锁指向 LockRecord(stack)

    • 在无冲突/争用轻微情况下,JVM 使用"轻量级锁"机制:线程在其栈上创建一个 LockRecord,并用 CAS 把对象头中的 mark word 替换为指向该 LockRecord 的信息(或者将 mark word 的值拷贝到 LockRecord 中,object header 存入指向 LockRecord 的指针/压缩形式,亦有实现细节差异)。当释放锁时通过 CAS 恢复原来的 mark word。

    • 用途:避免进入重量级(OS)互斥体,提高 uncontended case 的锁性能。

  4. 膨胀后(重量级)Monitor 指针

    • 当冲突严重或轻量级锁升级失败时,JVM 会"膨胀(inflate)"对象:为该对象分配一个 Monitor(在堆外或某个表中),并把对象头的 mark word 设置为 Monitor 的地址(或索引)。这个 Monitor 结构包含所有等待线程队列、计数器、持有线程等。

    • 用途:处理高争用的同步场景。

  5. identityHashCode(对象哈希码)

    • Java 的 System.identityHashCode(obj)(或 Object.hashCode() 未被覆写时)值需要在对象生命周期内稳定返回。通常 VM 在第一次计算或请求时将哈希值"缓存"在 mark word 的一些位域里(或在特殊情况下放到别处并在 mark word 放置指示)。

    • 注意 :哈希值与偏向锁/锁记录占用同一空间,二者互斥 ------ 例如对象偏向后若需要计算 identityHashCode,VM 需要撤销偏向或用其他机制处理冲突(这会使偏向效率受影响)。

  6. GC 年龄(age)位

    • 在 Young GC(复制算法)中,幸存对象的 age 会在 mark word 中记录(随着 Young GC 次数增加,age++)。当 age 达到一个阈值会 promote 到老年代。

    • 用途:分代策略与晋升决策。

    • 分代年龄(Age)的深入分析:

      复制代码
      class GenerationAgeAnalysis {
          
          // Age在Mark Word中的位置
          // 无锁/偏向锁状态:第3-6位(共4位)
          // 最大值为15(4位二进制1111)
          
          // 年龄递增逻辑
          public void incrementAge(Object obj) {
              // 每次Minor GC后,如果对象在Survivor区存活
              // 年龄加1
              
              // HotSpot源码示例:
              // markOop new_mark = mark->incr_age();
              // obj->set_mark(new_mark);
              
              // 晋升条件:
              // 1. 年龄达到-XX:MaxTenuringThreshold(默认15)
              // 2. 动态年龄判断:相同年龄对象总大小 > Survivor区一半
              // 3. 分配担保:Survivor空间不足
          }
          
          // 特殊年龄值
          class SpecialAgeValues {
              static final int AGE_NOT_INITIALIZED = 0;      // 初始年龄
              static final int AGE_FOR_TENURING = 15;        // 最大晋升年龄
              static final int AGE_FOR_PROMOTION = 7;        // 常用阈值(调优后)
          }
      }
  7. Forwarding pointer / GC 标记(在移动式 GC 中临时使用)

    • 在复制式 GC(如 HotSpot 的 Young GC)或压缩/移动过程中,对象可能从旧地址移到新地址。为避免寻找所有引用立即更新,GC 会在老地址的 mark word 中暂时写入forwarding pointer(新地址),并把原来的 mark word 暂存到新位置或其他地方。

    • 用途:在并行/并发/暂停-并行的移动收集过程中,临时记录移动信息。

  8. 其他控制位 / 保留位 / 版本/标识

    • 例如标记对象是否可偏向(biasable flag)、一些实现会记录 epoch 以便撤销偏向锁时做检查,或者 extra flag 用于特定 GC 的实现。

4. 状态转换示意(锁的典型生命周期)

下面给出典型的锁从无锁 → 偏向锁 → 轻量级锁 → 重量级锁(Monitor)的转换过程(HotSpot 的实现风格):

  1. 对象创建:unlocked/unbiased

    • mark word 处于"未锁定"语义(包含 age、可能空的 hash 等)。
  2. 第一次被线程 T1 同步(默认启偏向锁时)

    • VM 将对象头 CAS 为偏向信息(写入 T1 的线程 id),这样后续 T1 再次进入同步块无需 CAS(快速)。

    • 好处 :避免 uncontended synchronized 的开销。

  3. 另一个线程 T2 要锁该对象(出现争用)

    • 偏向需要撤销:VM 必须安全地撤销偏向(这涉及到偏向撤销协议,可能需要 safepoint 或 global revocation),并把状态转换为轻量级锁或直接膨胀为重量级锁。

    • 偏向撤销成本相对较高,因此在高争用场景中偏向锁并不总是优。

  4. 若争用短暂(两个线程短抢)

    • JVM 可能使用 轻量级锁:线程在自己栈上创建 LockRecord,并尝试在对象头做 CAS(把 mark word 暂存到 LockRecord 中并把对象头改为指向 LockRecord 的标识)。成功则继续;失败(另一线程也 CAS)说明争用激烈,可能膨胀到重量级锁。
  5. 若争用严重或某些条件触发

    • 升级为 重量级锁(Monitor):为对象或类分配 Monitor(内核互斥/条件变量),把对象头写成 Monitor 的指针;后续加锁/唤醒等进入 OS 同步机制。

这一套机制的设计目标是:对常见的无争用/短争用路径做非常快的处理(偏向/轻量),只有在确实存在高争用时才付出重量级成本。


5. 具体场景中 Mark Word 的使用

  • 调用 synchronized(obj)(无争用):若偏向锁启用且对象可偏向,mark word 会被写入偏向线程 id;不需要进入 OS。当锁释放且无别的线程请求,则保持偏向或回到未锁定状态(依实现)。

  • System.identityHashCode(obj)Object.hashCode() 首次调用:如果哈希值需要缓存,会在 mark word 中写入 hash(或 VM 使用旁边的表);这可能阻止之后对偏向锁机制的高效利用(因为同一 word 空间被占用)。

  • GC(Young GC)进行复制:对象被复制到新地址时,老地址的 mark word 会被暂时改为 forwarding pointer;GC 期间并发/并行线程会读取并据此完成引用重写。

  • 对象晋升/存活统计:每次 Young GC 存活,对象 age++(age 存在 mark word)。

  • 调试/逃逸分析/去优化:JIT 或 runtime 也会在某些场景读取 mark word 的状态来决定是否需要去偏(撤销偏向)或去优化/回退编译结果。


6. 标志位与位数注意

真实位布局随 JVM 版本、CPU 架构(x86_64 / aarch64 等)、以及 JVM 启动选项而不同。下面列出几个会影响的参数与事实(你在工程中需要注意):

  • -XX:+UseCompressedOops(指针压缩)会把对象引用从 8 字节变为 4 字节的压缩表示,影响对象的总体大小,但 mark word 本身 在 64-bit HotSpot 上通常仍是 8 字节(因为它需要和 CPU 原子操作对齐)。

  • -XX:+UseCompressedClassPointers(compressed klassptr)会使 klass pointer 从 8 → 4 字节,进而使头部从 16 → 12 bytes。

  • -XX:+UseBiasedLocking(启/禁偏向锁)会改变 mark word 在无锁情形时是否保存偏向线程 id 的语义。

  • GC 类型和实现(G1、ZGC、Parallel)会影响 forwarding pointer 在移动时的处理和是否把其他信息放到别处。

  • 因此精确到"某个位在第 13 位代表 hash 的第 5 位"这样的断言在不同平台上可能不成立 ------ 我会更强调"语义与状态转换",而不是在所有平台下精确的位偏移(这对理解机制和排查问题更有用且更稳健)。


7. 对象最小占用内存的推导

以现代 64-bit HotSpot(典型启用 compressed oops & compressed klassptr)为例计算最小对象大小:

  1. 对象头包含两块:

    • Mark Word:通常为 8 bytes(64-bit 字长对齐,便于原子 CAS 与 CPU 操作)。

    • Klass Pointer:若启用 compressed klassptr,4 bytes ;否则为 8 bytes。

      => 典型 compressed 情况:头=8+4=12 bytes

  2. 对象本体字段:若对象没有字段(空类),本体为 0 bytes。

  3. 对齐 :HotSpot 默认的对象对齐(-XX:ObjectAlignmentInBytes)通常 为 8 bytes 。JVM会把对象大小向上对齐到这个倍数。

    => 12 bytes 向上对齐到 8 的倍数是 16 bytes

所以空对象最小占用为 16 bytes 。如果不使用 compressed klassptr,头为 16 bytes,向上对齐后仍是 16 bytes(即最小仍然是 16)。在 32-bit 平台头为 8 bytes,向上对齐到 8 就是 8 bytes

数组对象 :数组对象除了上述头外还需要存储 length(通常 4 bytes)等,会使数组最小占用比普通对象更大;再按对齐补齐。


8. 为什么要对齐?为什么 Mark Word 通常为 8 bytes?

  • 对齐(alignment):提高内存/CPU 访问效率(缓存线、TLB),并且 CPU 原子操作(CAS)在某些宽度上更快或要求对齐。HotSpot 为了简单与性能通常采用 8 byte 对齐(甚至某些平台可更大)。

  • Mark Word 为 8 bytes 的原因:需要保证足够位宽以存储线程 id / hash / age / forwarding pointer 等信息,同时便于原子读写(64-bit 原子 CAS 在 64-bit CPU 上很常用)。即使后续压缩指针使得 klassptr 变小,mark word 仍通常保持 8 bytes。


9. 常见误解与注意点

  • "identityHashCode 会永久破坏偏向锁":更准确地说,哈希码的生成与偏向锁信息共享空间,会使 VM 采取撤销偏向或选择别的策略以保证哈希可用。效果是会使偏向锁不再对该对象有效或带来额外成本。

  • 不要把位宽数字当成 API 保证:位偏移和具体实现细节随 HotSpot 版本/patch 会变;不要依赖这些内部布局做二进制兼容。

  • 不同 GC 对 mark word 的使用不同:例如某些 GC 会把年龄放在 mark word 的特定位,另一些 GC 可能把年龄放到 side-table(旁表)中以保留 mark word 的其它用途。


10. 实用排查提示

  • 查看 JVM 参数java -XX:+PrintFlagsFinal -version | grep -i compressed 等,可确认 compressed oops/klassptr 是否启用。

  • 查看对象大小-XX:+UseCompressedOops-XX:ObjectAlignmentInBytes 影响大小;使用 java -agentlib:jdwp 等并非必要。可以通过小工具或 Instrumentation.getObjectSize(obj)(注意该 API 报告的是对象本体大小,不含对齐差异外的内存) 或者 jmap -histo / heap dump 再用 MAT 查看实际占用。

  • 观察锁行为 :用 -XX:+PrintSafepointStatistics / -XX:+PrintCompilation / Java Flight Recorder(JFR)观察偏向撤销、膨胀情况。 jstack 可以查看线程争用和 Monitor 信息。

  • 查看对象头内容(高级) :HotSpot 提供 unsafe/JOL(Java Object Layout)工具可以打印对象布局和 header 模拟(JOL 可以给很直观的对象内存分布示意)。


11. 小结

  • Mark Word 是一个可变解释的 word ,根据对象处于的**状态(未锁、偏向、轻量、重量、GC forwarding)**来解释其位域;它承载:hash、age、锁信息/线程 id、forwarding pointer 等。

  • 最小对象大小 由:markWord + klassPtr + fields 然后向上对齐 决定。常见:64-bit (compressed) → 最小 16 bytes32-bit → 最小 8 bytes

  • 场景:偏向锁用于无争用场景,轻量级锁用于短争用,重量级锁处理高争用;identityHashCode 会占用 mark word 空间;GC 在移动时会临时写 forwarding pointer 到 mark word。

  • 实现细节可变:具体位宽/位域会随 JVM 版本/参数/架构变化,不要在生产代码中做二进制依赖。

二、深入剖析多线程和同步

1. Java 线程在 OS 层的映射(创建、生命周期、栈)

  • 1:1 映射 :HotSpot(当前主流 JVM)把每个 java.lang.Thread 对象最终绑定到一个原生线程(POSIX pthread / Windows thread)。也就是说 Java 线程的调度与抢占由操作系统内核完成。创建 Java 线程会最终调用本地接口(JVM 层)去 pthread_create/CreateThread。(Stack Overflow)

  • 线程栈 :每个 Java 线程有自己的本地(native)线程栈 ,这栈既用于 JVM 本身的 C/C++ 代码,也用于解释器/编译器产生的 frame。Java 方法的局部变量与返回地址保存在 Java 栈帧,但底层使用 OS 分配的栈空间。-Xss 控制 Java 层可配置的栈大小(实质上配置的是 native stack 大小或 JVM 在创建 pthread 时设置的属性)。(注意:JVM 内部还维护"Java 栈帧"抽象,但物理上与 native stack 绑定)。(Pangin)

进程与线程的OS实现:

复制代码
// Linux内核中的进程/线程数据结构
struct task_struct {          // Linux进程/线程控制块
    volatile long state;       // 运行状态
    void *stack;              // 内核栈
    struct mm_struct *mm;     // 内存描述符
    pid_t pid;                // 进程ID
    struct list_head tasks;   // 任务链表
    struct thread_struct thread; // CPU上下文
    // ... 其他字段
};

// 三种线程模型对比
// 1. 用户级线程(1:M) - 由用户空间线程库管理
// 2. 内核级线程(1:1) - Java使用此模型
// 3. 混合线程模型(M:N)- Go使用此模型

// Java在Linux上的线程实现流程:
1. Java Thread.start()
   ↓
2. JVM调用pthread_create()创建内核线程
   ↓
3. Linux内核创建task_struct结构
   ↓
4. 分配内核栈(8KB-16KB)
   ↓
5. 设置CPU上下文(寄存器等)
   ↓
6. 加入调度队列等待CPU时间片

线程调度机制:

复制代码
// Linux内核调度器(CFS完全公平调度器)
struct sched_entity {
    struct load_weight load;    // 权重
    struct rb_node run_node;    // 红黑树节点
    unsigned int on_rq;         // 是否在运行队列
    u64 exec_start;             // 开始执行时间
    u64 sum_exec_runtime;       // 总执行时间
    // ...
};

// 调度优先级映射
// Java线程优先级 1-10 映射到 Linux nice值 [-20, 19]
// Java优先级6对应Linux nice值0(默认)
// 注意:Linux更关注公平性,Java优先级只是建议

// CPU时间片分配(典型值):
// 默认时间片:100ms(可配置)
// 最小粒度:1ms(受HZ配置影响,通常100HZ或1000HZ)
// 上下文切换成本:约1-5微秒

线程创建与销毁:

复制代码
// HotSpot VM中的线程创建流程
class JavaThread {
    // JVM层面的线程结构
    private OSThread _osthread;    // 操作系统线程句柄
    private JavaThreadState _state; // Java线程状态
    private jlong _thread_id;      // 线程ID
    
    // 线程创建本地方法(HotSpot源码片段)
    static void Thread::start(Thread* self) {
        // 创建OS线程
        os::create_thread(this, thr_type, stack_sz);
        
        // 设置线程属性
        pthread_attr_t attr;
        pthread_attr_init(&attr);
        pthread_attr_setstacksize(&attr, stack_size);
        
        // 创建内核线程
        int ret = pthread_create(&tid, &attr, 
                                (void* (*)(void*)) thread_native_entry, 
                                this);
    }
}

// 线程本地存储(TLS)实现
class ThreadLocalStorage {
    // Linux TLS实现(通过fs/gs寄存器)
    // Java中的ThreadLocal底层依赖TLS
    // 每个线程有独立的ThreadLocalMap
    
    // 内存布局:
    // +----------------------+
    // |    Java Thread对象    |
    // +----------------------+
    // |  ThreadLocalMap      |
    // |  +----------------+  |
    // |  | Entry[] table  |  |  // 哈希表存储线程本地变量
    // |  +----------------+  |
    // +----------------------+
    // |  操作系统线程栈       |  // 每个线程独立栈空间
    // +----------------------+
}

线程状态转换的OS实现:

复制代码
// Java线程状态与OS状态的对应关系
class ThreadStateMapping {
    // Java状态 → Linux状态
    static final Map<Thread.State, Integer> STATE_MAP = Map.of(
        Thread.State.NEW,        0,     // 创建中,对应task_struct创建
        Thread.State.RUNNABLE,   0,     // 对应TASK_RUNNING
        Thread.State.BLOCKED,    1,     // 对应TASK_INTERRUPTIBLE
        Thread.State.WAITING,    1,     // TASK_INTERRUPTIBLE
        Thread.State.TIMED_WAITING, 1,  // TASK_INTERRUPTIBLE
        Thread.State.TERMINATED, 2      // 对应EXIT_ZOMBIE
    );
    
    // Linux线程状态定义(include/linux/sched.h)
    // #define TASK_RUNNING        0    // 可运行或在运行
    // #define TASK_INTERRUPTIBLE 1    // 可中断的睡眠
    // #define TASK_UNINTERRUPTIBLE 2  // 不可中断的睡眠
    // #define TASK_STOPPED       4    // 停止
    // #define TASK_TRACED        8    // 被跟踪
    // #define EXIT_ZOMBIE        16   // 僵尸状态
    // #define EXIT_DEAD          32   // 死亡
}

线程栈内存管理:

复制代码
// Linux线程栈内存布局(64位系统)
+---------------------------------+ 高地址
|         Guard Page (4KB)        | ← 栈溢出保护
+---------------------------------+
|         线程栈空间               | 
|    (JVM默认1MB,可通过-Xss设置)   |
| +---------------------------+   |
| |   栈帧3 (Frame3)          |   |
| +---------------------------+   |
| |   栈帧2 (Frame2)          |   |
| +---------------------------+   |
| |   栈帧1 (Frame1)          |   |
| +---------------------------+   |
| |   ...                    |   |
| +---------------------------+   |
| |   主函数栈帧               |   |
| +---------------------------+   |
+---------------------------------+
|     未映射的Guard Page          |
+---------------------------------+ 低地址

// JVM栈帧结构
typedef struct stack_frame {
    void* return_address;     // 返回地址
    void* base_pointer;       // 基址指针(rbp)
    struct stack_frame* prev; // 前一个栈帧
    // 局部变量和操作数栈
} stack_frame_t;

// 栈溢出检测机制:
// 1. 通过Guard Page触发SIGSEGV信号
// 2. JVM信号处理器捕获并抛出StackOverflowError
// 3. 信号处理器:HotSpot/src/os/linux/vm/os_linux.cpp

2. 快路径 vs 阻塞路径 --- "尽量在用户态解决" 的原则

操作系统调用(系统调用)很贵 ------ 因为会发生内核切换、上下文切换、TLB/CPU pipeline flush 等成本。JVM 的设计哲学是:尽可能在用户态(CPU 指令 + 原子操作)完成锁的获取/释放 ,只有在出现真正的阻塞(需要睡眠)时才进入内核(调用 futex / pthread 等)。

  • 用户态快路径 :使用 CPU 原子指令(例如 x86 的 cmpxchg / lock cmpxchg),通过读写对象头(mark word)执行 CAS,完成无争用或短争用情形的同步(偏向锁 / 轻量级锁)。这些操作都是用户态内存读写 + 原子 CPU 指令,不涉及内核。

  • 失败时进入内核 :当多线程争用导致用户态 CAS 一直失败或需要阻塞等待时,JVM 会使用操作系统的等待/唤醒机制 ------ 在 Linux 上通常是 futex(fast userspace mutex),它允许"多数同步在用户空间完成;只有需要阻塞时再调用内核"。futex(2) 提供 FUTEX_WAIT/FUTEX_WAKE 等原语。(man7.org)

3. synchronized / monitor 的执行路径

当 JVM 执行 monitorenter / monitorexit(即 synchronized)时,一般分为以下步骤(高层):

  1. 读对象头(mark word):查看当前对象头的状态(unlocked / biased / lightweight-locked / inflated/monitor)。

  2. 试用用户态快路径

    • 若对象处于"未锁"且 biasing 条件允许,可能尝试写入偏向信息(偏向锁)。这是直接写 header(通常是一个写或 CAS)。偏向成功则无需进一步操作。

    • 若对象未偏向且没有争用,执行轻量级锁:线程在自己栈上创建 LockRecord,把原始 mark word 拷贝到 LockRecord,再用 CAS 把对象头置为表示"被该线程短暂占用"的值。成功后进入临界区;释放时用 CAS 恢复原头。整个过程在用户态完成。

  3. 若 CAS 多次失败(竞争真实存在) :升级为重量级锁(inflate -> Monitor) ,分配 Monitor 数据结构(含等待队列、owner、recursion count 等),并把对象头写为 monitor 指针(或用标识指向 side-table)。随后使用操作系统等待/唤醒机制阻塞等待线程(在 Linux 上通常底层借助 futex 或 pthread_cond/ mutex 封装)。Monitor 中会保存原始 mark word(hash 等)以便恢复。OpenJDK 的锁膨胀/对象 monitor 机制有多次演进(JDK 22 以后也有 ObjectMonitorTable 等改进)。(OpenJDK Wiki)

下面给出伪代码/序列(简化,表示快路径到阻塞路径):

复制代码
acquire_lock(obj):
  hdr = READ(obj.markword)
  if hdr == UNLOCKED and CAS(obj.markword, UNLOCKED, OWNED_BY_ME):
      return // got lock (fast)
  if hdr indicates BIASED to current_thread:
      return // fast (biased)
  // try lightweight
  orig = hdr
  make LockRecord on stack with displaced = orig
  if CAS(obj.markword, orig, ptr_to_LockRecord):
      return // lightweight success
  // failed many times -> inflate -> create Monitor
  monitor = create_monitor(obj, orig)
  // block using OS
  while true:
     if try_set_owner(monitor, current_thread):
         return
     wait_on_monitor(monitor) // uses futex/pthread_cond (kernel)
  • wait_on_monitor / wake 阶段会调用类似 futex_wait / futex_wake 或 pthread 条件变量;这一步会把线程挂到 kernel 块队列,发生内核态上下文切换。futex 的优势在于:没阻塞前不用进内核,减少 syscalls 。(Eli Bendersky's Website)

4. Linux 上的实际阻塞原语:futex 是怎样被 JVM 用到的?

  • 工作模式 :JVM 在对象头或 Monitor 中维护一个整型状态(通常是标志 + waiters count 等),用户态先做 CAS/自旋尝试获得锁;若失败并需要睡眠,才调用 futex(addr, FUTEX_WAIT, expected) 把当前线程挂起。其他线程释放锁时会 futex(addr, FUTEX_WAKE, n) 唤醒等待者。因为大多数锁争用是短暂的,绝大多数操作完成于用户态,进入内核的比例很小(因此性能好)。这也是 Linux pthread mutex 等库实现的常用策略。(man7.org)

5. park/unpark(LockSupport)与 JVM 的实现(OS 层)

  • Java 中 LockSupport.park() / unpark() 给并发包(java.util.concurrent)提供了低级阻塞原语。HotSpot 在运行时把 park/unpark 映射到 runtime 层的 park/unpark 实现(如 runtime/park.hpp 的 API),该实现最终会调用基于 OS 的事件(在 Linux 上可能是 futex,或使用 pthread_cond 封装),并处理 spurious wakeups。park 在用户态常常作为自旋+futex 的混合:先自旋短时间,再进入 futex wait。参见 HotSpot 源码 park.hpp。(GitHub)

    // LockSupport.park/unpark实现
    class Parker : public os::PlatformParker {
    private:
    // Linux使用pthread_cond_t
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
    int _counter; // 许可计数器

    public:
    void park(bool isAbsolute, jlong time) {
    pthread_mutex_lock(&_mutex);
    if (_counter > 0) {
    _counter = 0;
    pthread_mutex_unlock(&_mutex);
    return;
    }

    复制代码
          // 等待条件变量
          if (time == 0) {
              pthread_cond_wait(&_cond, &_mutex);
          } else {
              struct timespec ts;
              // 设置超时时间
              pthread_cond_timedwait(&_cond, &_mutex, &ts);
          }
          
          _counter = 0;
          pthread_mutex_unlock(&_mutex);
      }
      
      void unpark() {
          pthread_mutex_lock(&_mutex);
          int s = _counter;
          _counter = 1;
          if (s < 1) {
              pthread_cond_signal(&_cond);
          }
          pthread_mutex_unlock(&_mutex);
      }

    };

6. Safepoint、GC、偏向撤销:为什么 JVM 有时候需要"停全局线程"?

  • JVM 需要在做某些全局操作时保证所有 Java 线程都处于已知状态 (例如在做类卸载、全局 biased-lock 撤销、某些 GC 阶段、或生成/替换 JIT code 的一些操作时)。这就引入了 safepoint 机制:VM 发出 safepoint 请求,应用线程在"安全点"(safepoint poll) 停下并使 VM 能够操作其栈/寄存器/对象。safepoint 的实现使用轮询/页保护/信号等技巧以最小化代价(但仍然导致停顿)。偏向锁的批量撤销有时需要 safepoint(成本很高),因此偏向锁并非在所有竞争场景下都是 win。(Shipilev)

    // 偏向锁数据结构
    class MarkWord {
    // 偏向锁位布局(64位):
    // 54位:JavaThread* (偏向线程指针)
    // 2位:epoch (时间戳)
    // 1位:unused
    // 4位:age (分代年龄)
    // 1位:biased_lock(偏向锁标志)
    // 2位:lock(锁标志位)

    复制代码
      // 偏向锁撤销流程:
      // 1. 检查对象是否处于偏向模式
      // 2. 检查epoch是否过期
      // 3. 检查偏向线程是否存活
      // 4. 如果偏向线程不活跃,撤销偏向锁
      // 5. 如果偏向线程活跃但不在临界区,撤销
      // 6. 批量撤销/重偏向机制

    };

    // 为什么JDK15默认禁用偏向锁:
    // 1. 现代多线程应用竞争激烈,偏向锁收益小
    // 2. 撤销成本高(需要安全点STW)
    // 3. 增加了JVM复杂度
    // 4. 默认配置:-XX:-UseBiasedLocking

7. 内存语义(JMM)到 CPU/OS 的映射:内存屏障与原子指令

  • Java 的 happens-before/volatile/synchronized 语义最终由**CPU 内存屏障(memory fences)**和原子指令保证:

    • volatile 在生成机器码时会插入 load/store 屏障(或在 x86 上利用 lock 前缀/读写排序保证)。

    • synchronized 的 monitorenter/monitorexit 编译后涉及原子 CAS、锁内存屏障以及在必要时的 futex(阻塞/唤醒)。

  • JSR-133(Java Memory Model)定义了这些高层语义,而 HotSpot 在生成机器指令时把这些语义映射为 CPU 指令序列与屏障(并在多核下确保可见性)。

    // 现代CPU内存层次结构
    class CPUHierarchy {
    // 典型x86_64 CPU结构:
    // +------------------+
    // | Core 0 |
    // | +--------------+ |
    // | | L1 Cache | | // 64KB (32KB I + 32KB D)
    // | +--------------+ |
    // | | L2 Cache | | // 256KB-512KB
    // | +--------------+ |
    // +------------------+
    // | Core 1 |
    // | ... |
    // +------------------+
    // | 共享L3 Cache | // 8MB-64MB
    // +------------------+
    // | 主内存 DRAM | // 16GB-256GB
    // +------------------+

    复制代码
      // 缓存一致性协议:MESI
      // Modified(已修改), Exclusive(独占)
      // Shared(共享), Invalid(无效)
      
      // 内存访问延迟(近似):
      // L1 Cache: 1ns (1个时钟周期)
      // L2 Cache: 3-5ns
      // L3 Cache: 10-20ns
      // 主内存: 80-100ns

    }

8. 调度、优先级与上下文切换成本(OS 层)

  • 线程调度:Java 线程映射到 native 线程后受 OS 调度策略影响(Linux 的 SCHED_OTHER / CFS,实时策略可用但少用)。JVM 可以设置 thread priority,但 OS 对优先级的解释可能不同,且并不保证严格实时。

  • 上下文切换成本 :当一个线程被 futex/pthread_cond_wait 阻塞时,内核会切换到其它线程/进程,触发上下文切换(保存/恢复寄存器、TLB 影响、缓存失效等),延迟通常在微秒到毫秒量级,具体取决于负载与硬件。高争用导致频繁阻塞会显著降低吞吐与增加延迟。

  • 自旋 vs 阻塞:JVM 常用短自旋(spin)策略:先在用户态忙等待一小段时间(适合短锁持有场景,避免进入内核),自旋失败才阻塞;自旋次数与内核调度延迟需要在实践中折衷。

9. 本地方法 (JNI)、信号与特殊交互

  • JNI 本地线程:当 Java 调用本地方法并在本地阻塞时,native 线程会占用 OS 线程;为了不阻塞 VM 内部(例如 safepoint),HotSpot 有专门的本地/VM 线程概念与机制来处理 safepoint 与本地代码交互(例如将线程标记为 native 状态,使 safepoint 请求绕过或等待)。

  • 信号(signals)用法 :早期线程库与某些 VM 操作(也包括在调试/内部通信)使用 signals;HotSpot 在实现 safepoint、thread stack scanning、或者 platform-specific pause/restart 机制时,也需小心信号处理和与 JNI 的交互。现代实现更多依赖 futex/pthread primitives 而非频繁使用 signals。(Stack Overflow)

10. 端到端例子:一个典型 synchronized 在 Linux/x86 的行为序列(结合 OS 调用)

假设线程 T1 和 T2 同时 synchronized(obj)

  1. T1 执行 monitorenter:读 obj.markword → 未锁 → CAS 成功写入 lightweight owner(用户态成功)。(用户态,无内核)

  2. T2 试图 CAS 但失败(因为 header 已被 T1 改写) → T2 自旋几次(用户态短自旋) → 若依然失败,T2 调用 futex_wait(&monitor.waiters)(系统调用进入内核,挂起线程)。(此时上下文切换)

  3. T1 释放锁:将 obj.markword 恢复为 orig;然后调用 futex_wake(&monitor.waiters, 1)(系统调用唤醒 1 个等待线程)。内核唤醒后把 T2 放回可运行队列(另一次上下文切换)。

  4. T2 被调度运行并继续重试/获得锁。

这条路径展示了 用户态 CAS/SPI N → 内核 futex_wait/wake → 调度 的常见循环。

11. 性能实践建议(基于 OS 行为)

  • 尽量避免频繁阻塞/唤醒:阻塞涉及内核调用与上下文切换代价。对高 QPS/低延迟场景,建议使用无锁结构或避免长时间持有互斥锁。

  • 使用合适的数据结构java.util.concurrent 的并发集合/原子类在减少内核阻塞上通常更优(它们内部也会使用 park/unpark + CAS + occasional futex)。

  • 线程池而非频繁创建线程 :线程创建(pthread_create)也有系统成本,使用 ExecutorService 和线程池复用 OS 线程。

  • 控制栈大小-Xss 太大限制并发线程数,太小可能导致 StackOverflow;根据系统内存和线程数调整。

  • 避免偏向锁在高争用场景下带来的撤偏成本 :如果你的应用中同一对象频繁被不同线程竞争(很多互斥),可以考虑禁用偏向锁来避免批量撤偏的 safepoint 成本(但需基于 profiling 决定)。(Diva Portal)

12. 观察与调试(如何在 OS 层看到这些行为)

  • Linux perf / pidstat / htop:查看上下文切换、CPU 利用。

  • strace / perf trace :观察 futex / nanosleep / pthread_create 等系统调用。

  • jstack / jcmd / JFR / Java Flight Recorder:查看 Java 层线程堆栈、阻塞原因(monitor / waiting / timed_waiting)。

  • 结合系统工具 :例如在高延迟场景,用 strace -f -p <pid> 可以看到大量 futex syscalls,配合 JVM 线程 dump 分析 contention 来源。

  • 使用perf工具分析

    1. 监控Java线程的上下文切换

    perf stat -e context-switches,cpu-migrations -p <PID>

    2. 分析锁竞争

    perf record -e lock:lock_acquire -p <PID>
    perf report

    3. CPU热点分析

    perf record -g -p <PID> # 采样调用栈
    perf report --stdio

    4. 内存屏障开销

    perf stat -e mem_inst_retired.all_loads,mem_inst_retired.all_stores -p <PID>

    5. 获取火焰图

    perf record -F 99 -p <PID> -g -- sleep 60
    perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > flame.svg

三、深入剖析LockSupport.park()和unpark()

概要:

  • LockSupport.park():阻塞当前线程,直到:

    • 有可用的"permit"(许可)可消费(unpark(thread) 产生),或者

    • 当前线程被中断(Thread.interrupt()),或者

    • 被"虚假唤醒"(spurious wakeup),或者

    • 可选的超时到期(parkNanos/parkUntil 版本)。

  • LockSupport.unpark(thread):给指定线程至多一个 permit(布尔型,非计数信号)。如果目标线程已经在 park() 上阻塞,则会唤醒;如果目标线程尚未调用 park(),下次调用 park() 时会立即返回 ------ unpark 可以先于 park 调用 (不会丢失一次许可),但多次 unpark 不会累加(permit 只是一位,non-cumulative)。

  • 重要语义点:

    • park/unpark 与监视器 wait/notify 不同:不需要在 monitor 下使用;是 线程级独立的许可机制

    • park 可能返回(并非仅因为 unpark),因此代码通常需要用循环或状态检查(同 AQS 中的队列检查)。

    • park 对中断的处理:被中断会导致 park 返回并设置线程的中断状态(实现略有细节,但总体行为如此)。

1. LockSupport的设计哲学与架构

LockSupport在Java并发体系中的位置

复制代码
// LockSupport是AQS、Lock、同步器等的基础设施
// 架构层次关系:
+--------------------------------+
|        Java并发工具类           |  // ReentrantLock, CountDownLatch等
+--------------------------------+
|      AbstractQueuedSynchronizer|  // AQS,使用LockSupport阻塞/唤醒
+--------------------------------+
|         LockSupport             |  // 核心阻塞/唤醒原语
+--------------------------------+
|   操作系统线程调度机制           |  // pthread_cond, futex, epoll等
+--------------------------------+

// LockSupport的关键特性:
// 1. 许可证(permit)机制:每个线程有一个permit(0或1)
// 2. 提前unpark:先调用unpark再调用park不会阻塞
// 3. 可中断:park()响应中断,但不会抛出InterruptedException
// 4. 可超时:parkNanos()支持纳秒级超时
// 5. 与synchronized/wait-notify解耦:不要求持有任何锁

2. Parker对象:Linux平台实现

Parker数据结构与内存布局

复制代码
// HotSpot源码中的Parker定义(Linux版本)
// 文件:hotspot/src/os/linux/vm/os_linux.cpp

class Parker : public os::PlatformParker {
private:
    // Linux实现使用pthread的条件变量
    pthread_mutex_t _mutex[1];      // 互斥锁,保护_counter
    pthread_cond_t  _cond[1];       // 条件变量,用于等待
    int _counter;                   // 许可证计数器(0或1)
    int _cur_index;                 // 当前索引(用于解决ABA问题)
    
    // 线程指针,用于中断处理
    JavaThread* _thread;
    
    // 等待队列,用于记录park的线程
    ParkEvent* _event;
    
    // 保护_counter的原子性访问
    volatile int _allocated;        // 分配状态标志
    
public:
    Parker();
    ~Parker();
    
    void park(bool isAbsolute, jlong time);
    void unpark();
};

park()方法的完整执行路径

复制代码
// park()方法在Linux上的详细实现
void Parker::park(bool isAbsolute, jlong time) {
    // 第一步:快速路径检查(无锁)
    // 检查是否有可用的permit
    if (Atomic::xchg(0, &_counter) > 0) {
        // 有permit,立即返回(相当于消费了一个permit)
        return;
    }
    
    // 第二步:获取互斥锁,准备进入阻塞
    Thread* thread = Thread::current();
    assert(thread->is_Java_thread(), "Must be Java thread");
    
    // 设置线程状态为阻塞
    thread->set_state(_thread_blocked);
    
    // 获取互斥锁(进入临界区)
    pthread_mutex_lock(_mutex);
    
    // 第三步:重新检查_counter(防止竞争条件)
    // 在获取锁的过程中,可能已经有unpark()被调用
    if (_counter > 0) {
        // 有permit,释放锁并返回
        _counter = 0;
        pthread_mutex_unlock(_mutex);
        thread->set_state(_thread_runnable);
        return;
    }
    
    // 第四步:开始阻塞等待
    struct timespec absTime;
    
    if (time != 0) {
        // 计算超时时间
        if (isAbsolute) {
            // 绝对时间
            to_abstime(&absTime, time);
        } else {
            // 相对时间
            to_reltime(&absTime, time);
        }
    }
    
    // 进入条件变量等待循环
    int ret;
    while (_counter <= 0) {
        // 记录等待次数(用于诊断)
        thread->inc_park_count();
        
        if (time == 0) {
            // 无限期等待
            ret = pthread_cond_wait(_cond, _mutex);
        } else {
            // 限时等待
            ret = pthread_cond_timedwait(_cond, _mutex, &absTime);
            if (ret == ETIMEDOUT) {
                // 超时,退出循环
                break;
            }
        }
        
        // 检查中断
        if (thread->is_interrupted(false)) {
            // 线程被中断,退出等待
            break;
        }
    }
    
    // 第五步:消费permit(如果有)
    if (_counter > 0) {
        _counter = 0;
    }
    
    // 第六步:清理和返回
    pthread_mutex_unlock(_mutex);
    thread->set_state(_thread_runnable);
    
    // 确保内存可见性(StoreLoad屏障)
    OrderAccess::fence();
}

unpark()方法的详细实现

复制代码
// unpark()方法的完整实现
void Parker::unpark() {
    // 第一步:原子性地增加_counter
    // 使用CAS确保线程安全,避免锁开销
    int s = Atomic::xchg(1, &_counter);
    
    if (s < 1) {
        // _counter原来为0,可能有线程在等待
        // 需要获取锁并唤醒等待线程
        
        // 尝试获取互斥锁
        pthread_mutex_lock(_mutex);
        
        // 再次检查_counter(防止ABA问题)
        if (_counter > 0) {
            // 已经有其他线程进行了unpark,直接返回
            pthread_mutex_unlock(_mutex);
            return;
        }
        
        // 发送条件变量信号
        // 注意:使用pthread_cond_signal而不是pthread_cond_broadcast
        // 因为每个Parker只关联一个线程,只会有一个线程在等待
        int status = pthread_cond_signal(_cond);
        
        // 检查信号发送是否成功
        if (status != 0) {
            // 错误处理
            fatal("pthread_cond_signal failed with status = %d", status);
        }
        
        // 释放互斥锁
        pthread_mutex_unlock(_mutex);
    }
    // 如果s >= 1,说明permit已经存在,无需操作
    // 这实现了"一次性的permit"语义
    
    // 确保内存可见性
    OrderAccess::fence();
}

3. 操作系统级阻塞原语分析

pthread_cond_wait的Linux内核实现

复制代码
// pthread_cond_wait在glibc中的实现
// 文件:nptl/pthread_cond_wait.c

int __pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex) {
    struct _pthread_cleanup_buffer buffer;
    struct _condvar_cleanup_buffer cbuffer;
    int err, pshared, canceltype;
    
    // 检查条件变量和互斥锁有效性
    if (cond->__data.__total_seq < cond->__data.__wakeup_seq)
        return 0;
    
    // 获取当前时间(用于超时计算)
    struct timespec rt;
    clock_gettime(CLOCK_REALTIME, &rt);
    
    // 进入futex等待
    // 关键:使用futex系统调用实现高效阻塞
    int futex_val = cond->__data.__futex;
    
    do {
        // 调用futex进入内核等待
        int err = futex_wait ((int *) &cond->__data.__futex,
                             futex_val, NULL);
        
        if (err == -EINTR) {
            // 被信号中断
            // 检查是否应该继续等待
            if ((cond->__data.__flags & __PTHREAD_COND_CLOCK_MONOTONIC_MASK) != 0)
                continue;
        }
        
        // 更新等待序列号
        cond->__data.__woken_seq++;
        
    } while (cond->__data.__total_seq < cond->__data.__wakeup_seq);
    
    return 0;
}

// futex_wait的简化实现
static int futex_wait(int *uaddr, int val, const struct timespec *timeout) {
    return syscall(SYS_futex, uaddr, FUTEX_WAIT, val, timeout, NULL, 0);
}

// futex系统调用的内核实现(Linux kernel)
// 文件:kernel/futex.c
SYSCALL_DEFINE6(futex, u32 __user *, uaddr, int op, u32 val,
                const struct timespec __user *, utime,
                u32 __user *, uaddr2, u32 val3)
{
    struct timespec ts;
    ktime_t t, *tp = NULL;
    
    if (utime) {
        // 处理超时参数
        if (copy_from_user(&ts, utime, sizeof(ts)) != 0)
            return -EFAULT;
        
        t = timespec_to_ktime(ts);
        tp = &t;
    }
    
    // 根据操作类型分发
    switch (op & FUTEX_CMD_MASK) {
    case FUTEX_WAIT:
        // 关键:将线程加入等待队列
        return do_futex(uaddr, op, val, tp, uaddr2, val3);
    case FUTEX_WAKE:
        return do_futex(uaddr, op, val, tp, uaddr2, val3);
    }
    
    return -ENOSYS;
}

// do_futex的核心逻辑
static long do_futex(u32 __user *uaddr, int op, u32 val,
                     ktime_t *timeout, u32 __user *uaddr2, u32 val3)
{
    // 将用户空间地址转换为内核地址
    unsigned long futex_key = (unsigned long)uaddr;
    
    // 获取futex哈希桶
    struct futex_hash_bucket *hb;
    hb = hash_futex(&futex_key);
    
    // 自旋等待获取哈希桶锁
    spin_lock(&hb->lock);
    
    // 检查futex值是否与预期相同
    if (get_futex_value(uaddr) != val) {
        spin_unlock(&hb->lock);
        return -EAGAIN;
    }
    
    // 将当前任务加入等待队列
    struct futex_q q = {
        .task = current,
        .lock_ptr = &hb->lock,
        .key = futex_key,
    };
    
    // 将q加入等待队列
    __queue_me(&q, hb);
    
    // 设置任务状态为可中断睡眠
    set_current_state(TASK_INTERRUPTIBLE);
    
    // 释放哈希桶锁
    spin_unlock(&hb->lock);
    
    // 调度器选择其他任务运行
    schedule();
    
    // 当被唤醒后,清理等待队列条目
    unqueue_me(&q);
    
    return 0;
}

条件变量的性能优化

复制代码
// Linux内核中futex的优化实现
class FutexOptimizations {
    // 1. 快速路径:用户空间自旋
    // 在调用futex系统调用前,先在用户空间检查几次
    // 避免不必要的内核态切换
    
    static int futex_trylock_pi(u32 __user *uaddr) {
        u32 curval;
        
        // 尝试最多3次自旋
        for (int i = 0; i < 3; i++) {
            if (get_user(curval, uaddr) != 0)
                return -EFAULT;
                
            if (curval & FUTEX_OWNER_DIED)
                return 1;
                
            // 尝试原子交换
            if (cmpxchg_futex_value_locked(uaddr, curval, current->pid))
                return 0;
        }
        
        // 自旋失败,进入慢速路径(系统调用)
        return futex_lock_pi(uaddr, NULL, 0);
    }
    
    // 2. 等待队列优化:使用哈希表减少锁竞争
    // Linux使用futex哈希桶,将不同地址的futex分散到不同桶
    // 减少全局锁竞争
    
    // 3. 优先级继承:解决优先级反转问题
    // 当高优先级线程等待低优先级线程持有的锁时
    // 临时提升低优先级线程的优先级
};

4. 许可证(permit)机制的深度分析

permit的精确语义

复制代码
// LockSupport的permit机制代码分析
public class LockSupport {
    // HotSpot JVM中的permit实现
    // 每个Java线程都有一个permit(int类型,0或1)
    
    // permit的状态机:
    // +------------+  unpark()   +------------+
    // | permit = 0 | ----------> | permit = 1 |
    // +------------+             +------------+
    //       ^                          |
    //       |        park()            |
    //       +--------------------------+
    
    // 关键特性:
    // 1. 最多只有一个permit(不会累积)
    // 2. unpark()先于park()调用时,park()不会阻塞
    // 3. park()会消费permit(如果存在)
    // 4. permit是线程本地的
    
    // 伪代码实现:
    class ThreadLocalPermit {
        // 每个线程的permit状态
        // 实际存储在Parker::_counter中
        private int permit = 0;  // 0表示没有许可,1表示有许可
        
        void unpark() {
            // CAS操作,保证原子性
            if (permit == 0) {
                permit = 1;
                
                // 如果有线程在等待,唤醒它
                if (isThreadWaiting()) {
                    wakeUpThread();
                }
            }
            // 如果permit已经是1,什么都不做
        }
        
        void park() {
            // 检查是否有permit
            if (permit == 1) {
                // 有permit,消费它并立即返回
                permit = 0;
                return;
            }
            
            // 没有permit,进入阻塞状态
            permit = 0;  // 重置状态
            waitForUnpark();
        }
    }
}

permit与中断的交互

复制代码
// park()对中断的处理机制
void Parker::park(bool isAbsolute, jlong time) {
    // ... 前面代码省略
    
    // 进入条件变量等待
    while (_counter <= 0) {
        // 检查中断状态
        JavaThread* jt = (JavaThread*)Thread::current();
        
        if (jt->is_interrupted(false)) {
            // 线程已被中断,清除中断状态
            jt->set_interrupted(false);
            
            // Java层的LockSupport.park()会在此处返回
            // 但不会抛出InterruptedException
            // 调用者需要自己检查Thread.interrupted()
            
            // 释放锁并返回
            pthread_mutex_unlock(_mutex);
            thread->set_state(_thread_runnable);
            return;
        }
        
        // 执行条件变量等待
        if (time == 0) {
            pthread_cond_wait(_cond, _mutex);
        } else {
            pthread_cond_timedwait(_cond, _mutex, &absTime);
        }
        
        // 再次检查中断(可能在等待期间发生中断)
        if (jt->is_interrupted(false)) {
            // 处理同上
            jt->set_interrupted(false);
            pthread_mutex_unlock(_mutex);
            thread->set_state(_thread_runnable);
            return;
        }
    }
    
    // ... 后面代码省略
}

5. 性能优化与调优

park/unpark的性能基准

复制代码
// park/unpark性能测试与优化
public class LockSupportBenchmark {
    // 性能对比:
    // 操作                      | 平均耗时      | 说明
    // -------------------------|--------------|------------------------
    // synchronized进入/退出     | 15-20 ns     | 无竞争时,偏向锁
    // LockSupport.park()       | 800-1200 ns  | 包含系统调用开销
    // LockSupport.unpark()     | 200-400 ns   | 通常比park快
    // Object.wait()           | 1000-1500 ns | 需要持有锁
    // Thread.sleep(0)         | 500-800 ns   | 可能触发上下文切换
    
    // 影响性能的因素:
    // 1. 系统调用开销:从用户态切换到内核态(约100-200ns)
    // 2. 上下文切换:如果需要调度其他线程(约1-5μs)
    // 3. 缓存失效:线程迁移到不同CPU核心导致缓存失效
    // 4. NUMA架构:远程内存访问延迟
}

减少park/unpark系统调用的优化

复制代码
// 用户空间自旋优化
class SpinBeforePark {
    // 在进入内核态阻塞前,先在用户空间自旋一段时间
    // 这对短时间等待的场景特别有效
    
    static void park_with_spin() {
        // 第一阶段:快速检查(无锁)
        if (permit_available()) {
            consume_permit();
            return;
        }
        
        // 第二阶段:短时间自旋(约100-1000次循环)
        int spin_count = 0;
        const int MAX_SPIN = 1000;
        
        while (spin_count++ < MAX_SPIN) {
            // 短暂pause指令,减少CPU功耗
            asm volatile("pause" ::: "memory");
            
            if (permit_available()) {
                consume_permit();
                return;
            }
        }
        
        // 第三阶段:进入真正的park(系统调用)
        real_park();
    }
    
    // x86的pause指令作用:
    // 1. 提示CPU这是自旋等待循环
    // 2. 减少CPU功耗
    // 3. 避免内存顺序冲突
    // 4. 在超线程CPU上,让出资源给另一线程
};

批量唤醒优化

复制代码
// AQS中的批量唤醒优化
// AbstractQueuedSynchronizer在唤醒等待线程时
// 会尝试批量唤醒,减少系统调用次数

class AbstractQueuedSynchronizer {
    // 在释放锁时,批量唤醒等待线程
    private void unparkSuccessor(Node node) {
        // 获取等待队列中的下一个节点
        Node s = node.next;
        
        if (s == null || s.waitStatus > 0) {
            // 需要从尾部向前遍历找到有效的节点
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev) {
                if (t.waitStatus <= 0) {
                    s = t;
                }
            }
        }
        
        // 批量唤醒逻辑
        if (s != null) {
            // 如果有多个连续的可唤醒节点,尝试批量唤醒
            if (s.next != null && s.next.waitStatus <= 0) {
                // 这里可以优化为批量unpark
                // 但当前实现是逐个unpark
                unparkThread(s.thread);
                
                // 继续检查下一个节点
                Node next = s.next;
                while (next != null && next.waitStatus <= 0) {
                    unparkThread(next.thread);
                    next = next.next;
                }
            } else {
                // 单个唤醒
                unparkThread(s.thread);
            }
        }
    }
};

6. park/unpark在AQS中的应用

AQS中的等待队列管理

复制代码
// AQS如何使用park/unpark
public abstract class AbstractQueuedSynchronizer {
    // 等待队列节点
    static final class Node {
        volatile Thread thread;      // 等待的线程
        volatile Node next;         // 下一个节点
        volatile int waitStatus;    // 等待状态
        
        // 关键:线程在队列中通过LockSupport.park()阻塞
    }
    
    // 获取锁失败时,线程进入等待队列
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        
        // 快速入队:尝试CAS插入队尾
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        
        // 快速入队失败,进入完整入队流程
        enq(node);
        return node;
    }
    
    // 线程在队列中等待
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    // 成功获取锁
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                
                // 检查是否需要阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) {
                    // park()阻塞,直到被unpark()或中断
                    interrupted = true;
                }
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    // 阻塞并检查中断
    private final boolean parkAndCheckInterrupt() {
        // 调用LockSupport.park()阻塞线程
        LockSupport.park(this);
        
        // 被唤醒后,检查中断状态
        return Thread.interrupted();
    }
    
    // 释放锁时唤醒后继节点
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev) {
                if (t.waitStatus <= 0)
                    s = t;
            }
        }
        
        if (s != null) {
            // 关键:调用LockSupport.unpark()唤醒线程
            LockSupport.unpark(s.thread);
        }
    }
}

7. 核心要点总结

  1. 许可证机制:LockSupport使用一次性许可证,unpark发放,park消费

  2. 底层实现:Linux使用pthread条件变量+futex,Windows使用Event对象

  3. 性能特点:park()约800-1200ns,包含用户态到内核态切换开销

  4. 与中断关系:park()响应中断但不抛异常,需要调用者检查中断状态

  5. 虚假唤醒:必须用循环检查条件,不能依赖单次park()返回

最佳实践:

  1. 总是使用while循环检查条件,而不是if

  2. 在复杂同步场景中优先使用LockSupport而不是wait/notify

  3. 注意中断处理,及时清理中断状态

  4. 在高性能场景考虑减少park/unpark调用频率

  5. 监控线程状态,避免线程泄漏(park后永远无法唤醒)

四、深入剖析park/unparkwait/notify的区别

概览:

  • wait/notify 是基于对象监视器(monitor)的同步原语,严格绑定到 synchronized(必须持有监视器,wait 自动释放并重新获取锁),其可见性语义由 monitor release/acquire(JMM)保证,底层通常由 pthread mutex/condvar 或 JVM 的 monitor + futex 实现;适合与互斥、条件变量紧密耦合的情况。

  • park/unpark 是基于线程的 permit(单次许可)的低级阻塞原语,不依赖任何 monitor(可以在任意上下文使用),实现上通常用用户态原子+短自旋配合 futex/WaitOnAddress 等内核等待唤醒;它更灵活、轻量且更适合实现队列化等待(AQS、ForkJoin 等),也使 unpark 在语义上可以先于 park

1. 概念与 API 语义差异

  • 绑定关系

    • wait/notify:必须在持有对应对象监视器时调用(即在 synchronized(obj) 内部)。wait原子地释放监视器并阻塞 ,返回前将重新获取监视器

    • park/unpark:无须任何 monitor;任意线程可 park()(阻塞自身),任意线程可对任意 Thread 对象 unpark(thread)(产生许可)。因此 park 更自由、解耦。

  • 许可语义 vs 条件语义

    • park/unpark:基于per-thread permit(非累积的单比特许可)unpark 设置 permit(最多 1),park 消费 permit 或在没有时阻塞。unpark 先发生再 park 的情况不会丢失一次许可(若实现的 thread-state 已就绪);但 unpark 并不累计多次许可。

    • wait/notify:基于**monitor 的等待集合(wait set)**和 notify/notifyAllnotify 将一个等待线程从该对象的 wait set 中移动到可运行/准备获得锁的队列(实际是否被唤醒依赖调度和锁重新获取),不会在 Java 层留下"许可位"。

  • 中断/异常

    • wait:当线程在 wait 中被中断,会抛 InterruptedException(并且不会丢失中断状态,需 catch)。因此 wait 是可中断阻塞且以异常通知的语义。

    • park:被中断则返回且设置线程中断标志(Thread.interrupted());park 不抛出 InterruptedException(由调用方检查中断位)。

  • 先发唤醒(unpark-before-park)

    • park/unpark 支持 unparkpark 之前发生(通常会在实现上将 permit 写到线程的某个字段),从而保证随后一次 park 不会阻塞(这是 LockSupport 设计的一部分,但在极端实现时需注意 thread native init 时序)。

    • wait/notifynotifywait 之前调用会丢失(因为 wait 将线程加入 wait-set 是在调用时完成的),因此必须在 wait 已经发生时由另外线程 notify 才能唤醒 ------ 换句话说,notify 先于 wait 无保证(除非程序用额外的共享变量/标志做协调)。

2. Java 内存模型(JMM)与 happens-before 行为

核心事实(非常关键):

  • JMM 中定义的 synchronized (monitorenter/exit)建立明确的 happens-before :一次 monitor exit(释放锁)happens-before 之后任何成功的 monitor enter(获得同一监视器)。因此 wait 在释放与重新获得锁这段过程中,若一个线程在持有锁时修改了共享变量并 notify,并在释放锁后,这个 release happens-before 后续等待线程重新获得锁时看到的操作,从而保证可见性。

  • wait/notify 的可见性依赖于 监视器锁的 release/acquire 。具体:当通知者在持有锁的情况下修改共享数据并 notify,随后释放锁,等待者在从 wait 返回并重新获得锁后能看到这些修改(因为 monitor release/acquire 建立了 happens-before)。

  • park/unpark 并不是 JMM 的原生同步动作(它不是由 synchronizedvolatile 定义的),但 HotSpot/库在实现 park/unpark 时会使用原子操作、volatile 写/读或内存屏障来提供必要的内存可见性(例如 unpark 通常会先做一个 release 写到 permit,park 在消费时做 acquire),从而在实践上能实现与 synchronized 相似的可见性,但这需要上层正确使用(例如通常在 AQS 中,状态变更用 volatile/CAS,再 unpark 唤醒,park 返回后读取的状态能看到之前写入)。总结:wait 的可见性由 JMM 明确定义;park 的可见性依赖实现与上层约定(通常正确但不是 monitor semantics)

3. 底层实现与系统调用差异(用户态优先 vs kernel primitives)

wait/notify 实现(JVM 层)

  • wait():JVM 要做这些操作(抽象):

    1. 保证当前线程持有对象监视器(如果不是,抛 IllegalMonitorStateException)。

    2. 将当前线程加入对象的 wait set(monitor 的等待列表),这个操作在 JVM 内需要把线程入队(数据结构在 JVM 管理),并且原子地释放 monitor(把锁 owner 清空并唤醒可能的竞争者)

    3. 阻塞线程(通常通过 monitor 的等待结构调用底层阻塞 primitive):在 Linux/HotSpot 上,Monitor 的等待通常用 VM 内部的 Monitor / ObjectMonitor + futex 或 pthread_cond(具体实现多样)来完成阻塞(即可能调用 futex_waitpthread_cond_wait)。

    4. 当被 notify(或 notifyAll)唤醒/中断/超时时,等待线程会被放入与 monitor 关联的"试图重新获取锁"的队列,然后在可获取锁时再次进入并返回 wait()

  • notify()/notifyAll():调用必须在持有锁条件下执行,JVM 在 notify 时从 wait set 中选择一个线程(实现选择策略不保证公平,通常是任意或 FIFO),对该线程执行唤醒(把它从 wait set 中移动到要竞争锁的队列,可能进行 futex_wake 替代),但真正让该线程返回需要它重新获得 monitor(因此还依赖 monitor release/acquire 顺序)。

park/unpark 实现(HotSpot 常见)

  • park() 在用户态先尝试:

    • 检查 per-thread permit(atomic/volatile)------若存在,消费并直接返回(无需 syscall)。

    • 自旋短时间(减少短等待的 syscall)。

    • 若仍无 permit,设置 per-thread wait-state / park word(供 kernel futex 锁使用),然后进行第二次检查 (避免 lost-wakeup),最后调用 futex_wait(&parkword, WAITING, timeout?) 阻塞(或类似 WaitOnAddress/condvar)。

  • unpark(thread)

    • 在用户态写入 thread.permit = 1(release),然后如果目标线程处于 WAITING(park-state),执行 futex_wake(&parkword, 1);否则不做 kernel wake。
  • 关键点:大部分时间由用户态原子操作和自旋完成,只有在必要时才进行 futex 系统调用,从而减少内核切换成本(适合短阻塞)。这也是 LockSupport 被大量并发库采用的一个原因(更高效的阻塞语义)。

4. 时序与 lost-wakeup(为什么 wait/notify 无法先 notify,再 wait)

  • wait/notify

    • notifywait 之前调用通常会丢失信号 ,因为 notify 只能把线程从已有的 wait set 中移出;如果目标线程还没加入 wait set(没有调用 wait()),notify 没有地方把唤醒信息记着;因此正确的用法是结合一个共享条件(如 while (!condition) wait()),并在变更条件时 notify。这是为什么用 wait/notify必须在循环里检查条件的原因之一(不仅针对虚假唤醒,也避免 notify 先发导致丢失)。
  • park/unpark

    • unpark 可以在 park 之前执行(在多数实现中):因为 unpark 写入 thread.permit(线程本地的许可位),park 后续会看到 permit 并立即返回,而不会阻塞(这是 LockSupport 设计用于避免 lost-wakeup 的一项优点)。注意在某些极端时序或实现细节(如线程 native init 尚未完成)下,先 unpark 也可能"丢失",但在常规场景中可依赖。

5. 释放/重新获得锁(atomicity)与重入行为

  • wait()在调用时 JVM 会原子地释放监视器 ,这保证了"先放锁再阻塞"的原子性,从而不会发生 race(其它线程可以在你 wait 之后立即获得锁)。返回时 wait()重新获得监视器(阻塞时是无锁;返回后保证持锁)。这使得 wait 与 monitor 的组合非常适合条件变量的经典模式(producer/consumer)。

  • park()不自动释放任何锁 。如果线程在 synchronized 块里调用 park(),它不会自动释放监视器(并且通常你也不能这么做------会导致死锁)。因此 park 要与显式锁/队列/状态配合使用(例如 AQS 在调用 park 前已经将线程节点入队并释放了锁的状态,park 仅负责阻塞;唤醒后会通过 AQS 的机制尝试重新获得锁)。这就是为什么 AQS 等框架在使用 park 时需要有额外的状态控制。

6. 唤醒选择策略与可控性(公平性与唤醒范围)

  • notify:唤醒的是 wait set 中的某一个线程(非确定性)notifyAll 唤醒全部等待线程,但它们会争夺锁(可能引起大量竞争/thundering herd)。JVM 对 notify 的选择没有强保证(实现依赖 VM),通常不是公平队列式的。

  • unpark(thread):你可以精确指定要唤醒的线程(更精确),或由上层队列逻辑(AQS)保证唤醒 order(FIFO)。这使 park/unpark 更容易构建可控和可预测的唤醒策略(避免 notifyAll 的群体唤醒问题)。

7. 底层系统调用差别:pthread_cond vs futex(在 Linux 上的表现)

  • wait/notify(VM monitor 实现)通常会依赖:

    • POSIX 实现:pthread_cond_wait + pthread_mutex(这些内部也可能使用 futex);或

    • HotSpot 的 ObjectMonitor + futex 封装(JVM 实现把 monitor 的同步字放到一个 int 上,使用 futex_wait/wake)。

      这些 API 会做"释放互斥量并等待"的原子组合(pthread_cond_wait 内部保证了 release+wait 的原子性),但 pthread_cond 的语义是条件变量,并且仍有虚假唤醒的可能性(因此需要循环)。

  • park/unpark(LockSupport)通常直接用per-thread futex word (或 OS 等价)做 futex_wait/futex_wakefutex 的优势在于:用户态先尝试原子操作和自旋,只有在确实需要挂起时才发 syscall,从而减少 syscalls 与上下文切换。pthread_cond 的实现也可能做类似优化,但 park/unpark 是为高性能并发库专门设计的更直接原语。

8. 性能对比(可扩展性、系统调用成本、thundering herd)

  • syscall 频率与上下文切换park/unpark 的用户态优先策略(permit + spin)能在短阻塞场景下避免 syscalls,从而更高效。wait/notify 在简单实现上每次 wait/notify 都可能引起内核协助(取决于实现),因此在高争用场景或短等待场景下成本更高。

  • thundering herdnotifyAll 会唤醒全部等待者,造成抢锁/唤醒风暴;park/unpark 与 AQS 的单个唤醒策略(unpark 一个)更能避免群体唤醒,从而更可扩展。

  • 队列化设计park/unpark 更适合构建队列化等待(CLH/CLH-like queues),因为上层可以精确保存每个节点对应的线程并调用 unpark(thread) 来唤醒特定节点;wait/notify 更适用于简单条件下的协作,但不方便精确控制唤醒策略。

9. 可观测性、调试与常见陷阱

  • wait/notify 的常见错误:

    • 在没有持锁的情况下调用 waitnotifyIllegalMonitorStateException

    • 忘在循环里检查条件(while(!cond) wait())→ 可能于虚假唤醒或 notify 先发导致逻辑错误。

  • park/unpark 的常见错误:

    • 误用 unpark 在线程尚未初始化好(例如在 start 之前),可能出现"先 unpark 丢失"(实现相关的时序问题)。

    • 忽略中断标志(park 不抛异常),调用方需要显式检查中断位。

    • 直接把 park() 用在持有 monitor 内部(会导致死锁,除非你显式释放锁)。

  • 调试工具jstack 可显示 WAITING (parking)TIMED_WAITING (parking)。在 Linux 上 strace -f -e trace=futex -p pid 能看到 futex wait/wake;perf 可以显示自旋 vs syscall 的消耗。

10. 典型示例对比(伪代码 + 时序)

wait/notify 典型 producer/consumer

复制代码
// producer-consumer with wait/notify
synchronized(queue) {
   while (queue.isEmpty()) {
      queue.wait(); // atomically release lock and block
   }
   item = queue.remove();
}
synchronized(queue) {
   queue.add(item);
   queue.notify(); // must hold lock
}

时序要点:producer 在 notify 之前要持锁并完成对 queue 的修改,然后释放锁(release happens-before),等待的 consumer 在从 wait 返回且重新获得锁后能看到改动。

park/unpark 实现队列(AQS 样式,简化)

复制代码
// enq node and park
node = new Node(currentThread);
enqueue(node);
while (!tryAcquire()) {
   // release any held resource (already done by enqueue logic)
   LockSupport.park(node);
   // return -> loop checks node/owner again
}

// unpark one
Node next = dequeue();
if (next != null) LockSupport.unpark(next.thread);

时序要点:上层用 volatile/CAS 管理队列状态;在更改状态后调用 unpark,unpark 设置 permit + wake futex;park 释放资源的原子性由上层逻辑保证(不是 park 自动做的)。

11. 何时用哪个

  • 若你的同步严格依赖于 monitor(synchronized)语义、需要自动释放/重新获得锁、并且逻辑简单(例如 wait/notify 实现的典型生产者/消费者),使用 wait/notify(或 Condition)是直接且安全的选择。它与 JMM 的监视器语义匹配,容易推理。

  • 如果你在实现高性能并发结构(锁实现、队列、线程池、AQS、ForkJoinPool)并需要:可精确控制唤醒、支持 unpark 先行、减少 syscalls、避免 notifyAll 的 thundering-herd,或需要非监视器化的阻塞语义 ------ 那就用 park/unpark(通常配合 volatile/CAS 状态与队列结构)。

  • 生产实践:高性能库(J.U.C)都采用 park/unpark + AQS 来实现 ReentrantLock、Semaphore、CountDownLatch 等(AQS 提供可靠的队列化 + 状态变更 + memory visibility)。

12. 总结

  • wait/notify:monitor-bound、自动释放/重获锁、JMM 明确定义的 happens-before,可见性由 monitor release/acquire 保证,notify 先发会丢失,底层依赖 monitor/pthread_cond/futex;简单安全但不易扩展到高性能场景。

  • park/unpark:线程级 permit、可先 unpark 再 park、更加灵活与高效(用户态优先 + futex),适合并发框架实现,但需要上层正确的状态/队列管理来保证原子性(park 不会替你释放任何锁),且需手工处理中断/超时/条件检查。

  • 从操作系统层面看,关键区别是:wait/notify 更强调 monitor 的 release+wait 原子语义(由 VM/OS 提供),而 park/unpark 更强调用户态优先的单线程等待许可 + 内核等待(futex)的混合策略。因此二者在可扩展性、syscall 数量、唤醒策略和使用约束上有本质差异。

相关推荐
m0_719084112 小时前
滴滴滴滴滴
java·开发语言
张乔242 小时前
spring boot项目中设置默认的方法实现
java·数据库·spring boot
heartbeat..2 小时前
数据库性能优化:SQL 语句的优化(原理+解析+面试)
java·数据库·sql·性能优化
Qhumaing2 小时前
Java学习——第五章 异常处理与输入输出流笔记
java·笔记·学习
阿杰 AJie2 小时前
MyBatis-Plus 比较运算符
java·数据库·mybatis
码农幻想梦2 小时前
实验六 AOP,JdbcTemplate及声明式事务
java·开发语言·数据库
我是一只小青蛙8882 小时前
Python文件组织:路径抽象到安全归档
java·服务器·前端
XXOOXRT2 小时前
基于SpringBoot的用户登录
java·spring boot·后端
不穿格子的程序员2 小时前
JVM篇1:java的内存结构 + 对象分配理解
java·jvm·虚拟机·内存结构·对象分配