一、深入剖析Mark Word
1. Mark Word中的字段总揽
-
Mark Word 存储的"概念字段"(按用途分类,不按位数):
-
对象的 身份哈希值(identityHashCode)(若已计算并缓存)
-
对象的 GC 年龄(age)(用于分代晋升决策)
-
锁/线程相关信息(包括偏向锁的线程 id / epoch、轻量级锁所需的指针/指向栈上 LockRecord 的指针、或膨胀后的重量级监视器(Monitor)指针)
-
标志位/状态位(标识当前 mark word 存在的状态:未锁定、有偏向锁、处于轻量级锁、处于重量级锁、在 GC 时作为转发指针/标记等)
-
在移动式 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/版本/平台会有差别。我会在每个条目后说明常见的使用场景与转换。
-
Lock/state flag(状态标志位)
-
表示当前 mark word 正在以哪种"解释模式"存在:如
0 = unlocked、biased、lightweight、monitor、marked-for-GC/forward等。 -
用途:每次对对象加/解锁或 GC 发生时,首先读这些标志,决定如何解释其余位。
-
-
偏向线程 id / epoch(biased lock)
-
当启用偏向锁并且对象被某线程"偏向"(optimistically 偏向到第一个锁它的线程)时,mark word 中会存入该线程的 id(或线程指纹)及一个 epoch/age 字段。
-
用途场景 :无争用的
synchronized(obj),首次加锁的成本极低(只需原子操作设置 mark word),后续同一线程再次进入无需 CAS;在出现其他线程竞争时 VM 需要撤销偏向(偏向撤销会执行一些全局或局部操作)。
-
-
轻量级锁指向 LockRecord(stack)
-
在无冲突/争用轻微情况下,JVM 使用"轻量级锁"机制:线程在其栈上创建一个 LockRecord,并用 CAS 把对象头中的 mark word 替换为指向该 LockRecord 的信息(或者将 mark word 的值拷贝到 LockRecord 中,object header 存入指向 LockRecord 的指针/压缩形式,亦有实现细节差异)。当释放锁时通过 CAS 恢复原来的 mark word。
-
用途:避免进入重量级(OS)互斥体,提高 uncontended case 的锁性能。
-
-
膨胀后(重量级)Monitor 指针
-
当冲突严重或轻量级锁升级失败时,JVM 会"膨胀(inflate)"对象:为该对象分配一个 Monitor(在堆外或某个表中),并把对象头的 mark word 设置为 Monitor 的地址(或索引)。这个 Monitor 结构包含所有等待线程队列、计数器、持有线程等。
-
用途:处理高争用的同步场景。
-
-
identityHashCode(对象哈希码)
-
Java 的
System.identityHashCode(obj)(或Object.hashCode()未被覆写时)值需要在对象生命周期内稳定返回。通常 VM 在第一次计算或请求时将哈希值"缓存"在 mark word 的一些位域里(或在特殊情况下放到别处并在 mark word 放置指示)。 -
注意 :哈希值与偏向锁/锁记录占用同一空间,二者互斥 ------ 例如对象偏向后若需要计算 identityHashCode,VM 需要撤销偏向或用其他机制处理冲突(这会使偏向效率受影响)。
-
-
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; // 常用阈值(调优后) } }
-
-
Forwarding pointer / GC 标记(在移动式 GC 中临时使用)
-
在复制式 GC(如 HotSpot 的 Young GC)或压缩/移动过程中,对象可能从旧地址移到新地址。为避免寻找所有引用立即更新,GC 会在老地址的 mark word 中暂时写入forwarding pointer(新地址),并把原来的 mark word 暂存到新位置或其他地方。
-
用途:在并行/并发/暂停-并行的移动收集过程中,临时记录移动信息。
-
-
其他控制位 / 保留位 / 版本/标识
- 例如标记对象是否可偏向(biasable flag)、一些实现会记录 epoch 以便撤销偏向锁时做检查,或者 extra flag 用于特定 GC 的实现。
4. 状态转换示意(锁的典型生命周期)
下面给出典型的锁从无锁 → 偏向锁 → 轻量级锁 → 重量级锁(Monitor)的转换过程(HotSpot 的实现风格):
-
对象创建:unlocked/unbiased
- mark word 处于"未锁定"语义(包含 age、可能空的 hash 等)。
-
第一次被线程 T1 同步(默认启偏向锁时)
-
VM 将对象头 CAS 为偏向信息(写入 T1 的线程 id),这样后续 T1 再次进入同步块无需 CAS(快速)。
-
好处 :避免 uncontended
synchronized的开销。
-
-
另一个线程 T2 要锁该对象(出现争用)
-
偏向需要撤销:VM 必须安全地撤销偏向(这涉及到偏向撤销协议,可能需要 safepoint 或 global revocation),并把状态转换为轻量级锁或直接膨胀为重量级锁。
-
偏向撤销成本相对较高,因此在高争用场景中偏向锁并不总是优。
-
-
若争用短暂(两个线程短抢)
- JVM 可能使用 轻量级锁:线程在自己栈上创建 LockRecord,并尝试在对象头做 CAS(把 mark word 暂存到 LockRecord 中并把对象头改为指向 LockRecord 的标识)。成功则继续;失败(另一线程也 CAS)说明争用激烈,可能膨胀到重量级锁。
-
若争用严重或某些条件触发
- 升级为 重量级锁(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)为例计算最小对象大小:
-
对象头包含两块:
-
Mark Word:通常为 8 bytes(64-bit 字长对齐,便于原子 CAS 与 CPU 操作)。
-
Klass Pointer:若启用 compressed klassptr,4 bytes ;否则为 8 bytes。
=> 典型 compressed 情况:头=8+4=12 bytes。
-
-
对象本体字段:若对象没有字段(空类),本体为 0 bytes。
-
对齐 :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 bytes ;32-bit → 最小 8 bytes。 -
场景:偏向锁用于无争用场景,轻量级锁用于短争用,重量级锁处理高争用;identityHashCode 会占用 mark word 空间;GC 在移动时会临时写 forwarding pointer 到 mark word。
-
实现细节可变:具体位宽/位域会随 JVM 版本/参数/架构变化,不要在生产代码中做二进制依赖。
二、深入剖析多线程和同步
1. Java 线程在 OS 层的映射(创建、生命周期、栈)
-
1:1 映射 :HotSpot(当前主流 JVM)把每个
java.lang.Thread对象最终绑定到一个原生线程(POSIXpthread/ 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)时,一般分为以下步骤(高层):
-
读对象头(mark word):查看当前对象头的状态(unlocked / biased / lightweight-locked / inflated/monitor)。
-
试用用户态快路径:
-
若对象处于"未锁"且 biasing 条件允许,可能尝试写入偏向信息(偏向锁)。这是直接写 header(通常是一个写或 CAS)。偏向成功则无需进一步操作。
-
若对象未偏向且没有争用,执行轻量级锁:线程在自己栈上创建 LockRecord,把原始 mark word 拷贝到 LockRecord,再用 CAS 把对象头置为表示"被该线程短暂占用"的值。成功后进入临界区;释放时用 CAS 恢复原头。整个过程在用户态完成。
-
-
若 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):
-
T1 执行 monitorenter:读
obj.markword→ 未锁 → CAS 成功写入 lightweight owner(用户态成功)。(用户态,无内核) -
T2 试图 CAS 但失败(因为 header 已被 T1 改写) → T2 自旋几次(用户态短自旋) → 若依然失败,T2 调用
futex_wait(&monitor.waiters)(系统调用进入内核,挂起线程)。(此时上下文切换) -
T1 释放锁:将
obj.markword恢复为 orig;然后调用futex_wake(&monitor.waiters, 1)(系统调用唤醒 1 个等待线程)。内核唤醒后把 T2 放回可运行队列(另一次上下文切换)。 -
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>可以看到大量futexsyscalls,配合 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 report3. CPU热点分析
perf record -g -p <PID> # 采样调用栈
perf report --stdio4. 内存屏障开销
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. 核心要点总结
-
许可证机制:LockSupport使用一次性许可证,unpark发放,park消费
-
底层实现:Linux使用pthread条件变量+futex,Windows使用Event对象
-
性能特点:park()约800-1200ns,包含用户态到内核态切换开销
-
与中断关系:park()响应中断但不抛异常,需要调用者检查中断状态
-
虚假唤醒:必须用循环检查条件,不能依赖单次park()返回
最佳实践:
-
总是使用while循环检查条件,而不是if
-
在复杂同步场景中优先使用LockSupport而不是wait/notify
-
注意中断处理,及时清理中断状态
-
在高性能场景考虑减少park/unpark调用频率
-
监控线程状态,避免线程泄漏(park后永远无法唤醒)
四、深入剖析park/unpark和 wait/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/notifyAll。notify将一个等待线程从该对象的 wait set 中移动到可运行/准备获得锁的队列(实际是否被唤醒依赖调度和锁重新获取),不会在 Java 层留下"许可位"。
-
-
中断/异常
-
wait:当线程在wait中被中断,会抛InterruptedException(并且不会丢失中断状态,需 catch)。因此wait是可中断阻塞且以异常通知的语义。 -
park:被中断则返回且设置线程中断标志(Thread.interrupted());park不抛出InterruptedException(由调用方检查中断位)。
-
-
先发唤醒(unpark-before-park)
-
park/unpark支持unpark在park之前发生(通常会在实现上将 permit 写到线程的某个字段),从而保证随后一次park不会阻塞(这是 LockSupport 设计的一部分,但在极端实现时需注意 thread native init 时序)。 -
wait/notify:notify在wait之前调用会丢失(因为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 的原生同步动作(它不是由synchronized或volatile定义的),但 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 要做这些操作(抽象):-
保证当前线程持有对象监视器(如果不是,抛 IllegalMonitorStateException)。
-
将当前线程加入对象的 wait set(monitor 的等待列表),这个操作在 JVM 内需要把线程入队(数据结构在 JVM 管理),并且原子地释放 monitor(把锁 owner 清空并唤醒可能的竞争者)。
-
阻塞线程(通常通过 monitor 的等待结构调用底层阻塞 primitive):在 Linux/HotSpot 上,Monitor 的等待通常用 VM 内部的 Monitor / ObjectMonitor + futex 或 pthread_cond(具体实现多样)来完成阻塞(即可能调用
futex_wait或pthread_cond_wait)。 -
当被
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:
notify在wait之前调用通常会丢失信号 ,因为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_wake。futex的优势在于:用户态先尝试原子操作和自旋,只有在确实需要挂起时才发 syscall,从而减少 syscalls 与上下文切换。pthread_cond的实现也可能做类似优化,但park/unpark是为高性能并发库专门设计的更直接原语。
8. 性能对比(可扩展性、系统调用成本、thundering herd)
-
syscall 频率与上下文切换 :
park/unpark的用户态优先策略(permit + spin)能在短阻塞场景下避免 syscalls,从而更高效。wait/notify在简单实现上每次 wait/notify 都可能引起内核协助(取决于实现),因此在高争用场景或短等待场景下成本更高。 -
thundering herd :
notifyAll会唤醒全部等待者,造成抢锁/唤醒风暴;park/unpark与 AQS 的单个唤醒策略(unpark 一个)更能避免群体唤醒,从而更可扩展。 -
队列化设计 :
park/unpark更适合构建队列化等待(CLH/CLH-like queues),因为上层可以精确保存每个节点对应的线程并调用unpark(thread)来唤醒特定节点;wait/notify更适用于简单条件下的协作,但不方便精确控制唤醒策略。
9. 可观测性、调试与常见陷阱
-
wait/notify的常见错误:-
在没有持锁的情况下调用
wait或notify→IllegalMonitorStateException。 -
忘在循环里检查条件(
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 数量、唤醒策略和使用约束上有本质差异。