park与futex系统级协同机制解析
- 前言
- 系统级协同过程简述
- park与futex系统级协同机制解析
-
- [第一点:Glibc 层面的下推 ------ 从 `pthread_cond_wait` 到 `futex` 系统调用](#第一点:Glibc 层面的下推 —— 从
pthread_cond_wait到futex系统调用) - [第二点:Linux 内核层的状态演变、队列托管与 CFS 调度离队](#第二点:Linux 内核层的状态演变、队列托管与 CFS 调度离队)
-
- [1. 内核 Futex 层的挂起与排队 (`kernel/futex.c`)](#1. 内核 Futex 层的挂起与排队 (
kernel/futex.c)) - [2. CFS 调度器层面的离队 ------ 红黑树的剥离 (`kernel/sched/core.c`)](#2. CFS 调度器层面的离队 —— 红黑树的剥离 (
kernel/sched/core.c))
- [1. 内核 Futex 层的挂起与排队 (`kernel/futex.c`)](#1. 内核 Futex 层的挂起与排队 (
- 总结:数据结构的精确归宿
- [第一点:Glibc 层面的下推 ------ 从 `pthread_cond_wait` 到 `futex` 系统调用](#第一点:Glibc 层面的下推 —— 从
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正
系统级协同过程简述
当线程在 ObjectMonitor 中彻底拿不到锁、且经过多轮努力后,它最终会调用 os::PlatformEvent::park()(在 Linux 上对应 os::park())。
从系统级视角来看其简略过程如下:
-
JVM 内部为每个线程维护了一个
PlatformEvent(或Parker)实例,其底层封装了一个操作系统的 互斥量(pthread_mutex_t) 和 条件变量(pthread_cond_t)。 -
在 Linux 层面,
pthread_cond_wait最终会被下推为futex(Fast Userspace Mutex)系统调用。 -
内核将该线程的
task_struct状态修改为TASK_INTERRUPTIBLE,放入内核的全局红黑树等待队列中,彻底交出 CPU 执行权。
park与futex系统级协同机制解析
我们从系统角度深入探讨 JVM 线程从用户态下沉到内核态的过程,需要将视角从 OpenJDK 源码延伸到 Glibc (NPTL 线程库) 以及 Linux 内核 (Kernel) 的核心调度源码。
下面针对第1点(pthread_cond_wait 下推为 futex 系统调用)和第2点(内核修改 task_struct 状态、从 CFS 红黑树移出并交出 CPU)进行深度的源码级分析与注释。
第一点:Glibc 层面的下推 ------ 从 pthread_cond_wait 到 futex 系统调用
在 OpenJDK 8u 的 os::PlatformEvent::park() 中,最终触发的是 pthread_cond_wait(_cond, _mutex)。这个函数并不是 Linux 内核直接提供的,而是由 Linux 的用户态标准 C 库 Glibc 中的 NPTL (Native POSIX Thread Library) 实现的。
在 Glibc 源码中,pthread_cond_wait 的核心逻辑是管理用户态的条件变量内部状态(如等待序列号、锁标志等),并在发现条件不满足时,通过 Futex (Fast Userspace Mutex) 原子地挂起线程。
以下是 Glibc 中 pthread_cond_wait 调用 futex 的核心源码逻辑与注释:
c
// 源码路径:glibc/nptl/pthread_cond_wait.c
// 提示:为了便于阅读,已对复杂的取消点(Cancellation points)和版本兼容宏进行了精简,保留最核心的底层逻辑。
int __pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex)
{
struct _pthread_cleanup_buffer buffer;
struct pthread *self = THREAD_SELF; // 获取当前用户态线程的描述符(pthread 结构体)
int err, private;
// 1. 释放用户态传入的 mutex 锁
// 因为在进入 park 之前,JVM 已经通过 pthread_mutex_lock 加了锁,这里必须释放,
// 否则其他线程(如执行 unpark 的线程)将无法获取锁来改变状态或唤醒当前线程。
err = __pthread_mutex_unlock_usercnt (mutex, 0);
if (err != 0)
return err;
// 2. 确定 Futex 的作用域标志
// JVM 内部的锁通常是进程私有的(Private),不需要跨进程共享。
// __FUTEX_PRIVATE 告诉内核不需要处理跨进程的内存映射,从而极大地提高内核检索效率。
private = __condvar_wflags (cond) & FUTEX_PRIVATE_FLAG;
// 3. 循环挂起,防止内核的虚假唤醒(Spurious Wakeup)
while (1)
{
// 读取当前的条件变量序列号/信号值(g_signals)
unsigned int signals = cond->__data.__g_signals[g];
if (signals == 0)
{
// =====================================================================
// 核心下推:调用底层的宏 futex_wait_cancelable
// 该宏最终会转换为一个标准的 Linux 系统调用:sys_futex
// =====================================================================
/*
参数解析:
- &__g_signals[g]:用户态内存地址。内核会检查这个地址上的值。
- FUTEX_WAIT_KEY_VAL:期望值。内核会原子地检查该地址的值是否仍为 0(即无信号),
如果是,则挂起;如果已被修改,说明错过了唤醒,立即返回。
- private:私有标志(FUTEX_WAIT_PRIVATE)。
*/
err = futex_wait_cancelable (&cond->__data.__g_signals[g],
FUTEX_WAIT_KEY_VAL, private);
}
// 如果被唤醒,或者系统调用返回,检查是否获取到了信号,若获取到则打破循环
if (cond_has_signals_p(cond))
break;
}
// 4. 从内核被唤醒退出后,必须重新获取 mutex 锁,以维持 pthread_cond_wait 的语义规范
return __pthread_mutex_cond_lock (mutex);
}
// -------------------------------------------------------------------------
// 进一步追踪 futex_wait_cancelable 的展开(sysdeps/nptl/futex-internal.h)
// -------------------------------------------------------------------------
static inline int
futex_wait_cancelable (unsigned int *futex_word, unsigned int expected, int private)
{
// 最终通过 INLINE_SYSCALL 宏生成汇编指令(如 x86_64 上的 `syscall`),陷入内核态
// __NR_futex 是 Linux 内核分配给 futex 机制的全局系统调用号(在 x86_64 上通常是 202)
return INLINE_SYSCALL (futex, 4, futex_word, FUTEX_WAIT, expected, private);
}
第二点:Linux 内核层的状态演变、队列托管与 CFS 调度离队
当执行到汇编指令 syscall 时,CPU 从用户态(Ring 3)切换至内核态(Ring 0),并根据系统调用号 __NR_futex 跳转到内核的 sys_futex() 入口。
在内核中,这一步的逻辑演变为:
- 将线程引用的
task_struct放入一个基于哈希桶的等待队列中(用于未来被精确唤醒)。 - 将该线程的状态修改为
TASK_INTERRUPTIBLE(可中断的睡眠状态)。 - 触发内核主调度器
schedule(),由 CFS(完全公平调度器) 将其从运行红黑树中彻底剥离。
1. 内核 Futex 层的挂起与排队 (kernel/futex.c)
c
// 源码路径:linux/kernel/futex.c (以 Linux 4.x/5.x 经典稳定版内核为例)
// 这是 sys_futex 系统调用的核心处理函数
static int futex_wait(u32 __user *uaddr, unsigned int flags, u32 val,
ktime_t *abs_time, u32 bitset)
{
struct futex_hash_bucket *hb;
struct futex_q q = FUTEX_Q_INIT; // 初始化一个代表当前阻塞上下文的 futex 队列节点
int ret;
// 1. 初始化 futex_q 的一些必要参数,绑定当前的 task_struct(即当前线程)
q.bitset = bitset;
q.task = current; // current 是一个宏,指向当前正在 CPU 上运行的进程/线程的 task_struct
// 2. 根据用户态传入的内存地址(uaddr),通过哈希算法找到对应的内核全局哈希桶(Hash Bucket)
// Linux 内核为了并发性能,维护了一个全局的 futex_queues 哈希表,哈希桶内部是双向链表。
hb = futex_q_lock(uaddr, flags, &q);
// 3. 再次在内核态检查用户态地址的值是否等于期望值 val(双重检查,防止时序竞争)
ret = futex_lookup_with_key(uaddr, flags, &q, val);
if (ret) {
// 如果值已经变了,说明用户态有更新(有线程释放了锁或发出了信号),不能挂起
futex_q_unlock(hb);
return ret;
}
// 4. 进入真正的排队与挂起等待函数
futex_wait_queue_me(hb, &q, timeout);
// ... 线程在此处被挂起,直至未来被唤醒后才会执行下面的代码 ...
return 0;
}
// 具体的排队和状态修改函数
static void futex_wait_queue_me(struct futex_hash_bucket *hb, struct futex_q *q,
struct hrtimer_sleeper *timeout)
{
// 【核心逻辑 A】: 修改当前线程的 task_struct 状态为 TASK_INTERRUPTIBLE
// 此时线程虽然改变了状态,但还在 CPU 上运行,只是打上了"可中断阻塞"的标签
set_current_state(TASK_INTERRUPTIBLE);
// 【核心逻辑 B】: 将当前线程封装的 futex_q 插入到哈希桶(hb)的等待队列中
// 注意:这里插入的是 Futex 专用的双向链表(plist),用于在未来被其他线程执行 sys_futex(FUTEX_WAKE) 时进行 O(1) 级的快速检索。
plist_add(&q->list, &hb->chain);
// 释放哈希桶的锁,因为下面马上要让出 CPU 了,不能带着 spinlock 锁睡觉
spin_unlock(&hb->lock);
// 【核心逻辑 C】: 真正触发内核调度,交出 CPU 控制权
if (likely(q->task)) {
// 如果当前线程没有未处理的信号(Signals),则正式调用内核主调度器
if (!signal_pending(current)) {
// =========================================================
// 关键点:进入内核调度核心,引发上下文切换(Context Switch)
// =========================================================
schedule();
}
}
// =====================================================================
// 线程被唤醒后的恢复逻辑:
// 当另外的线程在 Java 层调用了 unpark(),或者重量级锁释放唤醒了该线程,
// 内核会将该线程的状态重新恢复,并从 schedule() 的下一行(即这里)醒来。
// =====================================================================
__set_current_state(TASK_RUNNING); // 重新将状态改回"可运行态"
}
2. CFS 调度器层面的离队 ------ 红黑树的剥离 (kernel/sched/core.c)
当上述代码执行到 schedule() 后,内核将启动进程/线程调度算法。
在这里,需要消除一个常见的误区:Linux 内核在 Futex 阻塞时,并不是把线程放到"一个全局的红黑树等待队列"中。 正确的底层逻辑是:内核将线程的 task_struct 放入 Futex 的哈希链表 后,执行 schedule(),调度器发现该线程状态为 TASK_INTERRUPTIBLE,便将其从当前 CPU 的 CFS(完全公平调度器)运行红黑树 中彻底移除(即"离队")。
以下是调度器执行离队的核心源码:
c
// 源码路径:linux/kernel/sched/core.c
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
// 闭锁或关中断,开始进行调度切换
do {
preempt_disable();
__schedule(false); // 调用内部主调度函数
sched_preempt_enable_no_resched();
} while (need_resched());
}
// 实际的内核主调度函数
static void __sched __schedule(bool preempt)
{
struct rq *rq;
struct task_struct *prev, *next;
unsigned int cpu;
cpu = smp_processor_id();
rq = cpu_rq(cpu); // 获取当前 CPU 核心专属的运行队列(Runqueue)
prev = rq->curr; // prev 指向当前准备交出 CPU 的线程(即我们那个获取不到锁的 Java 线程)
// 1. 检查准备交出 CPU 的线程状态
// 因为前面设置了 set_current_state(TASK_INTERRUPTIBLE),所以这里 prev->state != TASK_RUNNING 成立
if (!preempt && prev->state) {
if (signal_pending_state(prev->state, prev)) {
// 如果线程虽然是阻塞态,但有未处理的信号,则将其状态恢复为 TASK_RUNNING,不移出运行队列
prev->state = TASK_RUNNING;
} else {
// =========================================================
// 【核心逻辑 D】: 调用 deactivate_task 将该线程从运行队列中剥离!
// =========================================================
deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
// 标记该线程处于非激活/睡眠状态
prev->on_rq = 0;
}
}
// 2. 从当前 CPU 核心的可运行红黑树中,挑选下一个最适合运行的线程
// 此时由于 prev 已经被移出了红黑树,调度器不可能再选到它
next = pick_next_task(rq, prev, &rf);
// 3. 上下文切换:彻底交出 CPU
if (likely(prev != next)) {
// 切换 CPU 的寄存器上下文(如 RSP, RIP, CR3 页面映射树等)
// 执行完这一行后,CPU 开始执行 `next` 线程的代码,当前 Java 线程彻底失去了 CPU 执行权
rq = context_switch(rq, prev, next, &rf);
}
}
// -------------------------------------------------------------------------
// 深入追踪 deactivate_task() -> dequeue_task()
// 源码路径:linux/kernel/sched/fair.c
// -------------------------------------------------------------------------
static void dequeue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &p->se; // 获取该线程对应的 CFS 调度实体
// 逐步向上遍历进程组/调度组,将其从 CFS 的红黑树中彻底移除
for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
// =========================================================
// 【关键动作】: 调用底层红黑树擦除函数
// 将 se->run_node 从 cfs_rq->tasks_timeline(CFS 运行红黑树)中摘除
// =========================================================
__dequeue_entity(cfs_rq, se);
// 更新红黑树的权重、可运行线程数(h_nr_running--)等统计指标
update_load_avg(cfs_rq, se, UPDATE_TG);
}
}
总结:数据结构的精确归宿
全链路走完后,该 Java 线程在操作系统的真实状态如下:
- 用户态视角 (
ObjectMonitor&PlatformEvent) :线程对象被挂在ObjectMonitor的_cxq或_EntryList链表中,处于逻辑上的锁等待状态。 - 内核态同步视角 (
Futex) :线程的task_struct引用被存放在内核的futex_hash_bucket对应的双向冲突链表 中。未来当占有锁的线程释放并调用unpark/FUTEX_WAKE时,内核会根据用户态内存地址直接在这个链表中找到并唤醒它。 - 内核态调度视角 (
CFS Scheduler) :线程的状态为TASK_INTERRUPTIBLE。它已经从当前 CPU 核心的 CFS 运行红黑树 中被剥离(__dequeue_entity),因此在被重新唤醒(状态改回TASK_RUNNING并重新加入红黑树)之前,它绝对不会消耗任何 CPU 时间片。