在 Java 并发学习中,lock-support 的 park()
与 interrupt()
常被认为是"中断后再次 park 不会阻塞"。但经过一系列精心设计的 Demo 测试,发现了一个令人意外的现象:
- 当中断状态为
true
时,再次调用park()
的确不阻塞,这是符合预期的行为。 - 当用
Thread.interrupted()
清除中断标记后,预期下次park()
会立即阻塞,但实测却发现:- 连续两次调用
park()
才能真正阻塞线程。 - 更奇妙的是,仅修改了日志打印方式(参数化 vs 字符串拼接),竟能让"第二次 park 一次就阻塞"!
- 连续两次调用
本文深入拆解这一行为背后的核心原因:
- permit(许可)与 Java 中断标志是两套独立机制 :
interrupt()
会隐式调用unpark()
,设置 permit=1;park()
消耗 permit,触发阻塞或继续运行;Thread.interrupted()
只清除 Java 标志,不影响 permit。
- 日志格式、线程调度等细微时序变化会影响 permit 的设置和消费时机,从而决定
park()
是否阻塞。
1. 中断状态为 true 时, park 不阻塞
首先我们都认为:打断 Park 阻塞的线程时,如果打断标记已经是 true, 再次调用 park() 时是无法阻塞线程的
验证上面的知识点,代码如下
java
private static void interruptPark() throws InterruptedException {
Thread interruptParkThread = new Thread(() -> {
Thread thread = Thread.currentThread();
for (int i = 0; i < 5; i++) {
log.debug("第 {} 次执行 park 操作", i + 1);
LockSupport.park();
log.debug("线程继续运行,当前打断状态为:{}", thread.isInterrupted());
}
}, "interruptParkThread");
// 开启线程
interruptParkThread.start();
// 等待线程进入 park
TimeUnit.SECONDS.sleep(1);
log.debug("执行 interrupt 打断操作...");
interruptParkThread.interrupt();
}
java
23:42:47.069 - 第 1 次执行 park 操作
23:42:48.078 - 执行 interrupt 打断操作...
23:42:48.078 - 线程继续运行,当前打断状态为:true
23:42:48.078 - 第 2 次执行 park 操作
23:42:48.078 - 线程继续运行,当前打断状态为:true
23:42:48.078 - 第 3 次执行 park 操作
23:42:48.078 - 线程继续运行,当前打断状态为:true
23:42:48.078 - 第 4 次执行 park 操作
23:42:48.078 - 线程继续运行,当前打断状态为:true
23:42:48.078 - 第 5 次执行 park 操作
23:42:48.078 - 线程继续运行,当前打断状态为:true
2. 清除中断标志后,需要连续两次 park 才阻塞
那么我就又想到,如果打断标记是 false,使用 LockSupport.park() 的时候,应该会阻塞线程吧?
当我书写下面的 Demo 想要验证上面的想法的时候,就出现了一个有趣的现象:
打断标记是 false 时,第一次使用 LockSupport.park() 并没有阻塞线程,当我连续调用两次 LockSupport.park() 才将线程阻塞住!
有趣的是,我调整了一下日志的输出,既然惊奇的发现恢复打断标记为 false 之后,只需要一次 LockSupport.park() 就能够阻塞线程了,详细请看下面的 Demo 和对应的输出
java
private static void interruptPark() throws InterruptedException {
Thread interruptParkThread = new Thread(() -> {
Thread thread = Thread.currentThread();
for (int i = 0; i < 5; i++) {
// thread.isInterrupted() 只判断当前线程是否被打断,不会清空打断标记
if (thread.isInterrupted()) {
// Thread.interrupted() 判断当前线程是否被打断,但会清除打断标记
Thread.interrupted();
// 这一步查看打断标记一定是 false
log.debug("interrupted 清除打断标记, 打断状态一定为false, 查看结果为:{}", thread.isInterrupted());
}
try {
TimeUnit.SECONDS.sleep(1); // 睡眠一秒,在进行打断
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("第 " + (i + 1) + " 次执行 park 操作");
LockSupport.park();
log.debug("线程继续运行,当前打断状态为:{}", thread.isInterrupted());
}
}, "interruptParkThread");
// 开启线程
interruptParkThread.start();
// 等待线程进入 park
TimeUnit.SECONDS.sleep(2);
log.debug("执行 interrupt 打断操作...");
interruptParkThread.interrupt();
}
java
09:54:55.537 - 第 1 次执行 park 操作
09:54:56.545 - 执行 interrupt 打断操作...
09:54:56.545 - 线程继续运行,当前打断状态为:true
09:54:56.546 - interrupted 清除打断标记, 打断状态一定为false, 查看结果为:false
09:54:57.561 - 第 2 次执行 park 操作
09:54:57.561 - 线程继续运行,当前打断状态为:false
09:54:58.565 - 第 3 次执行 park 操作
当我连续调用两次 LockSupport.park() 才将线程阻塞住
java
private static void interruptPark() throws InterruptedException {
Thread interruptParkThread = new Thread(() -> {
Thread thread = Thread.currentThread();
for (int i = 0; i < 5; i++) {
// thread.isInterrupted() 只判断当前线程是否被打断,不会清空打断标记
if (thread.isInterrupted()) {
// Thread.interrupted() 判断当前线程是否被打断,但会清除打断标记
Thread.interrupted();
// 这一步查看打断标记一定是 false
log.debug("interrupted 清除打断标记, 打断状态一定为false, 查看结果为:{}", thread.isInterrupted());
}
try {
TimeUnit.SECONDS.sleep(1); // 睡眠1秒,在进行打断
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("第 " + (i + 1) + " 次执行 park 操作");
LockSupport.park();
if(i == 1){
LockSupport.park(); // 连续两次 park
}
log.debug("线程继续运行,当前打断状态为:{}", thread.isInterrupted());
}
}, "interruptParkThread");
// 开启线程
interruptParkThread.start();
// 等待线程进入 park
TimeUnit.SECONDS.sleep(2);
log.debug("执行 interrupt 打断操作...");
interruptParkThread.interrupt();
}
java
09:56:12.675 - 第 1 次执行 park 操作
09:56:13.664 - 执行 interrupt 打断操作...
09:56:13.664 - 线程继续运行,当前打断状态为:true
09:56:13.666 - interrupted 清除打断标记, 打断状态一定为false, 查看结果为:false
09:56:14.678 - 第 2 次执行 park 操作
3. 日志书写方式影响 park 执行结果
下面的 Demo 中,我仅仅调整了日志的输出,我想让日志书写规范一点,所以将 log.debug("第 " + (i + 1) + " 次执行 park 操作"); 修改为了 log.debug("第 " + (i + 1) + " 次执行 park 操作");
有趣的事情就发现了...我并没有使用两次 park,但是在第二次 park 线程的时候,是成功阻塞的!
java
private static void interruptPark() throws InterruptedException {
Thread interruptParkThread = new Thread(() -> {
Thread thread = Thread.currentThread();
for (int i = 0; i < 5; i++) {
// thread.isInterrupted() 只判断当前线程是否被打断,不会清空打断标记
if (thread.isInterrupted()) {
// Thread.interrupted() 判断当前线程是否被打断,但会清除打断标记
Thread.interrupted();
// 这一步查看打断标记一定是 false
log.debug("interrupted 清除打断标记, 打断状态一定为false, 查看结果为:{}", thread.isInterrupted());
}
try {
TimeUnit.SECONDS.sleep(1); // 睡眠1秒,在进行打断
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("第 {} 次执行 park 操作", i + 1);
LockSupport.park();
log.debug("线程继续运行,当前打断状态为:{}", thread.isInterrupted());
}
}, "interruptParkThread");
// 开启线程
interruptParkThread.start();
// 等待线程进入 park
TimeUnit.SECONDS.sleep(2);
log.debug("执行 interrupt 打断操作...");
interruptParkThread.interrupt();
}
java
09:56:49.762 - 第 1 次执行 park 操作
09:56:50.772 - 执行 interrupt 打断操作...
09:56:50.773 - 线程继续运行,当前打断状态为:true
09:56:50.773 - interrupted 清除打断标记, 打断状态一定为false, 查看结果为:false
09:56:51.786 - 第 2 次执行 park 操作
4. park 的底层原理
为了探究是什么原因造成了这个奇怪的现象,我发现了 park 和 interrupt 的一些底层原理,用来解释这个现象
每个线程都有自己的一个 Parker 对象(由C++编写,java中不可见),由三部分组成 _counter , _cond 和 _mutex 打个比喻
- 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)
- 调用 park 就是要看需不需要停下来歇息
- 如果备用干粮耗尽,那么钻进帐篷歇息
- 如果备用干粮充足,那么不需停留,继续前进
- 调用 unpark,就好比令干粮充足
- 如果这时线程还在帐篷,就唤醒让他继续前进
- 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
- 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 0,这时获得 _mutex 互斥锁
- 线程进入 _cond 条件变量阻塞
- 设置 _counter = 0

- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 唤醒 _cond 条件变量中的 Thread_0
- Thread_0 恢复运行
- 设置 _counter 为 0
5. 实验现象讨论
<font style="color:rgb(77, 77, 77);">LockSupport</font>
内部的许可是线程私有的,底层是通过 <font style="color:rgb(77, 77, 77);">Unsafe.park</font>
实现,许可保存在线程的对象内部,JVM 并没有为开发者暴露任何查询接口
但是查询资料得知 JVM 的特性
- Thread.interrupted() 不会清除 Parker 的 permit,Thread.interrupted() 只清除 Thread 对象上的 中断标志位(Java 层面),不会影响底层的 park permit
- permit 是由 LockSupport.unpark() 或底层 JavaThread::interrupt() 调用 parker()->unpark() 设置的,一旦设置,将一直保留,直到被 park() 消耗掉
- 《LockSupport 文档》明确指出 permit 最多只有一个;park() 会消耗,而 unpark() 重发许可
- docs.oracle.com/javase/8/do...
java
// interrupted方法调用native方法interrupt0(),而interrupt0()的c++源码如下
osthread()->set_interrupted(true);
_SleepEvent->unpark(); // 唤醒 Thread.sleep()
parker()->unpark(); // 唤醒 LockSupport.park()
_ParkEvent->unpark(); // 唤醒 synchronized / wait()
- permit 与中断标志是完全独立的两套机制
- Parker 内部维护的是一个 二元许可状态(permit=0/1),与中断标志是两套机制
- JavaThread::interrupt() 会同时:
- 设置中断标志;
- 调用 _SleepEvent.unpark()、parker()->unpark()、_ParkEvent.unpark(),从而触发许可或唤醒阻塞线程
所以即使后来 Thread.interrupted() 清除了中断标志,许可依然存在,可以被下一次 park() 直接消耗并立即返回
java
第 1 次执行 park 操作
执行 interrupt 打断操作...
线程继续运行,当前打断状态为:true
interrupted 清除打断标记... false
第 2 次执行 park 操作
对于上面输出的 permit 状态的演变:
- 起初 permit = 0 ➝ 第一次 park() 阻塞;
- interrupt() 调用:设置 permit = 1 + 中断标志 = true;
- 第一次 park() 被唤醒并 consumes permit ➝ permit = 0;
- Thread.interrupted() 清除中断标志,无 permit 损耗;
- 第二次 park():permit=0 ➝ 阻塞成功。
- ✅ 精准验证了 permit 被第一次 park() 而非 Thread.interrupted() 消耗了。
至于为什么出现参数化日志导致"不阻塞"的现象,我的解释是
- 参数化日志
log.debug("第 {} 次执行 park 操作",...)
会 推迟参数替换和字符串拼接,影响代码执行顺序; - 这可能导致第二次
park()
调用发生在 permit 还没被消费的位置; - 所以就出现了"第二次不阻塞"的现象,但这来源于时序微调,而非机制本身的变化
总结
最后来一个总结
- interrupt() 会 隐式执行 unpark,增加 permit,但 permit 是一次性变量,会被下一次 park() 消耗
- Thread.interrupted() 只影响 Java 层的中断标志,并不会触发任何 permit 操作;而 thread.interrupt() 会调用底层的 unpark,从而增加 permit。permit 是否被 park 消耗决定了线程是否阻塞
- permit 的存在与否决定 park() 是否阻塞,日志与时序影响可能导致行为微妙差异
- 使用 park() 时需谨慎管理许可状态、线程中断以及防虚假唤醒