一、前言
在Java并发编程中,AQS (AbstractQueuedSynchronizer) 是实现锁(如 ReentrantLock)、同步器(如 CountDownLatch)的核心基础。而 AQS 能够实现线程的阻塞与唤醒,其底层完全依赖于 LockSupport 工具类。
LockSupport 是 JUC 包中的一个工具类,主要用于挂起(block)和唤醒(wake up)线程。它所有的方法都是静态的,底层通过 Unsafe 类实现。
二、基本使用
LockSupport 主要是通过 park() 和 unpark(Thread thread) 来实现的。
-
park(): 阻塞当前线程,直到获得"许可"或被中断。
-
unpark(Thread thread): 给指定线程发放一个"许可",如果该线程被阻塞,则将其唤醒。
1. 标准用法:先 park 后 unpark
这是最直观的用法,主线程让子线程阻塞,然后三秒后唤醒它。
java
import java.util.concurrent.locks.LockSupport;
public class ParkUnparkDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " --- 1. 开始运行");
// 阻塞当前线程
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " --- 3. 被唤醒");
}, "t1");
t1.start();
// 睡眠3秒,确保t1先执行并进入park状态
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " --- 2. 发起唤醒");
// 唤醒线程t1
LockSupport.unpark(t1);
}
}
运行结果:
codeText
t1 --- 1. 开始运行
main --- 2. 发起唤醒
t1 --- 3. 被唤醒
2. 特殊用法:先 unpark 后 park
这是 LockSupport 与 Object.wait() 最大的区别之一。
即使 unpark 操作发生在 park 之前,线程依然可以正常通过(不会永久阻塞)。
java
import java.util.concurrent.locks.LockSupport;
public class UnparkFirstDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
// 让子线程睡一会儿,保证主线程先执行 unpark
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " --- 2. 准备park");
// 因为之前已经有许可了,这里不会阻塞,直接通过
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " --- 3. park结束,继续执行");
}, "t1");
t1.start();
System.out.println(Thread.currentThread().getName() + " --- 1. 提前发放许可");
// 提前给t1线程发放许可
LockSupport.unpark(t1);
}
}
原理简述:unpark 就像是给线程的钱包里预存了一张票,等线程执行 park 需要买票进站时,发现钱包里已经有票了,直接刷票进站,无需等待。
三、与 Object.wait/notify 的区别(重点)
在面试和实际开发中,这两组机制经常被拿来对比:
|----------|----------------------------------------------------------|-------------------------------------------------|
| 特性 | Object.wait() / notify() | LockSupport.park() / unpark() |
| 所属对象 | Object 类的方法 | LockSupport 类的静态方法 |
| 锁的要求 | 必须在 synchronized 代码块中执行,否则抛 IllegalMonitorStateException | 不需要获取任何锁,随时随地可调用 |
| 锁的释放 | 执行 wait 会释放当前持有的锁 | 执行 park 不会释放当前持有的锁(容易死锁,需注意) |
| 执行顺序 | 必须 先 wait 后 notify。如果先 notify 后 wait,线程会永久等待 | 顺序无关。可以先 unpark 存入许可,后 park 消费许可 |
| 中断响应 | 抛出 InterruptedException 异常 | 不抛异常,park 方法直接返回,需要通过 Thread.interrupted() 检查状态 |
四、底层原理深度解析
LockSupport 的底层是一个 native 方法,最终调用的是 JVM 内部(C++实现)的 Parker 对象。核心理念是 "Permit"(许可)。
1. Permit (许可) 机制
我们可以将 Permit 理解为一个二元信号量 (0 或 1),它不累加。
-
park() 流程:
-
检查当前线程是否有许可(Permit)。
-
如果有(Permit = 1):消耗许可(Permit 变为 0),park 方法立即返回,线程继续运行。
-
如果没有(Permit = 0):阻塞当前线程,直到被唤醒。
-
-
unpark(Thread t) 流程:
-
将指定线程的许可设置为 1(Permit 变为 1)。
-
注意: 因为是二元信号量,多次调用 unpark 不会叠加。unpark 100次,Permit 依然是 1。此时调用一次 park 就会消耗掉这个许可,第二次调用 park 依然会阻塞。
-
2. 源码级验证 (HotSpot VM)
在 JVM 源码(os_linux.cpp 和 os_windows.cpp 等)中,每个 Java 线程都有一个对应的 C++ 类 Parker 实例。
Parker 类主要包含三个核心变量:
-
_counter: 记录许可数量(0 或 1)。
-
_mutex: 互斥锁(用于保护操作)。
-
_cond: 条件变量(用于线程挂起)。
Java底层图示:



C++ 伪代码逻辑演示
park() 的大致逻辑:
cpp
void Parker::park(bool isAbsolute, jlong time) {
// 1. 如果已有许可(_counter > 0),直接消耗许可并返回
if (_counter > 0) {
_counter = 0;
return;
}
// 2. 获取互斥锁
pthread_mutex_lock(_mutex);
// 3. 再次检查许可(防止并发情况)
if (_counter > 0) {
_counter = 0;
pthread_mutex_unlock(_mutex);
return;
}
// 4. 真正阻塞,等待信号(类似Java的wait)
// 这里会释放锁并等待,直到被unpark唤醒或中断
pthread_cond_wait(_cond, _mutex);
// 5. 唤醒后,将许可归零
_counter = 0;
pthread_mutex_unlock(_mutex);
}
unpark() 的大致逻辑:
java
void Parker::unpark(Thread* thread) {
// 1. 获取互斥锁
pthread_mutex_lock(_mutex);
// 2. 只有当许可为0时,才设置为1(不可累加)
// 但无论之前是多少,最终状态都是1
int s = _counter;
_counter = 1;
// 3. 如果线程之前是阻塞状态(s < 1),唤醒它
if (s < 1) {
// 发送信号唤醒在 _cond 上等待的线程
pthread_cond_signal(_cond);
}
pthread_mutex_unlock(_mutex);
}
五、常见面试题与坑
1. 为什么推荐使用 park(Object blocker)?
在 Java 6 之后,LockSupport 新增了 park(Object blocker) 方法。
-
blocker 是用来标识当前线程在等待什么对象(通常传入 this)。
-
作用: 当使用 jstack 或 Profiler 工具排查死锁或性能问题时,能够显示出线程是被哪个对象 block 住的,极大地方便了问题定位。
对比:
-
park() 在 dump 文件中显示:parking to wait for <0x0000000000000000> (无信息)
-
park(this) 在 dump 文件中显示:parking to wait for <0x000000076b3820f0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
2. 中断(Interrupt)能唤醒 park 吗?
能。
当一个线程被 park 阻塞时,如果其他线程调用了该线程的 interrupt() 方法,park 会立即返回。
注意: 它不会抛出 InterruptedException,你需要手动检查中断标志位。
java
Thread t1 = new Thread(() -> {
LockSupport.park();
// 这里的代码会在 unpark 或 interrupt 后执行
if (Thread.currentThread().isInterrupted()) {
System.out.println("被中断唤醒了");
}
});
t1.start();
t1.interrupt(); // 线程会立刻从 park 中醒来
3. 多次 unpark,能否抵消多次 park?
不能。
java
LockSupport.unpark(Thread.currentThread());
LockSupport.unpark(Thread.currentThread()); // 此时 permit 还是 1
LockSupport.park(); // 消耗 permit (1 -> 0),通过
LockSupport.park(); // permit 为 0,阻塞!
六、总结
-
LockSupport 是 JUC 并发包的基石,AQS 依靠它来挂起和唤醒线程。
-
它利用 Permit(许可) 机制,解耦了阻塞和唤醒的顺序限制(可以先唤醒后阻塞)。
-
它底层利用操作系统的 Mutex 和 Condition Variable 实现,轻量且高效。
-
在使用时,要注意它不释放锁的特性,避免造成死锁。