Java的LockSupport与线程阻塞唤醒
在多线程编程中,线程间的协调至关重要。传统的wait()/notify()机制存在严格的顺序要求和持有监视器锁的限制。LockSupport类提供了一种更底层、更灵活的线程阻塞与唤醒机制,是构建高级并发工具的基础。
一、LockSupport是什么?
LockSupport是java.util.concurrent.locks包下的一个工具类,核心功能是阻塞(park) 和唤醒(unpark) 指定线程。它直接使用许可(permit)机制,不依赖任何监视器锁。
二、核心方法速查
| 方法名 | 作用 | 参数说明 |
|---|---|---|
void park() |
阻塞当前线程,直到许可可用或被中断。 | 无 |
void parkNanos(long nanos) |
阻塞当前线程,最多等待指定时间(纳秒)。 | nanos:最大等待纳秒数 |
void parkUntil(long deadline) |
阻塞当前线程,直到指定的绝对时间戳(毫秒)。 | deadline:绝对时间戳 |
void unpark(Thread thread) |
唤醒(使能)指定线程。如果线程正在park中,则唤醒它;如果还未park,则为其后续的park()调用提供许可。 |
thread:目标线程 |
三、与传统wait/notify的关键区别
理解LockSupport的优越性,需要对比传统方式。
| 对比维度 | Object.wait()/notify() |
LockSupport.park()/unpark() |
|---|---|---|
| 前置条件 | 必须在synchronized块内调用,必须持有监视器锁。 |
无限制,可以在任何地方调用。 |
| 唤醒顺序 | notify()是随机唤醒一个等待线程,无法指定。notifyAll()唤醒所有。 |
unpark(thread)精准唤醒指定线程。 |
| 阻塞/唤醒顺序 | 必须先wait(),后notify()。如果先notify(),信号会丢失。 |
顺序更灵活。可以先unpark()再park(),线程会立即返回(消耗许可)。 |
| 状态传递 | 基于对象监视器(条件队列)。 | 基于线程的“许可”标志。 |
四、如何使用LockSupport:手把手指南
步骤1:理解“许可”机制
LockSupport的核心是每个线程关联一个布尔型的“许可”。
- 默认状态:许可为
false。 - 调用
park():检查许可。- 若许可为
true,则立即将其置为false并返回,线程不会阻塞。 - 若许可为
false,则阻塞线程。
- 若许可为
- 调用
unpark(thread):将目标线程的许可置为true。如果该线程正在park()中,则被唤醒。
步骤2:实现基本阻塞与唤醒
import java.util.concurrent.locks.LockSupport;
public class BasicParkDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("工作线程:准备进入阻塞...");
// 步骤A: 阻塞当前线程
LockSupport.park();
System.out.println("工作线程:被唤醒,继续执行!");
});
worker.start();
// 主线程休眠1秒,确保工作线程已启动并park
Thread.sleep(1000);
System.out.println("主线程:唤醒工作线程");
// 步骤B: 唤醒指定的worker线程
LockSupport.unpark(worker);
}
}
执行结果:
- 工作线程启动后打印第一条信息,然后调用
park()阻塞。 - 主线程休眠后打印第二条信息,然后调用
unpark(worker)。 - 工作线程被唤醒,打印第三条信息。
步骤3:处理“先unpark后park”的场景
这是LockSupport的优势场景,避免了信号丢失。
import java.util.concurrent.locks.LockSupport;
public class UnparkFirstDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("工作线程:即将park...");
// 步骤B: 调用park。由于之前unpark已设置了许可,此处会立即返回。
LockSupport.park();
System.out.println("工作线程:park返回,继续执行!");
});
worker.start();
// 主线程休眠很短时间,目的是让工作线程启动,但不一定已执行到park
Thread.sleep(100);
System.out.println("主线程:提前给工作线程发放许可(unpark)");
// 步骤A: 先于park调用unpark。这会将worker的许可设为true。
LockSupport.unpark(worker);
}
}
执行结果:
即使unpark发生在park之前,工作线程的park()调用也会立即返回,因为它检测到了有效的许可。
步骤4:实现可中断的阻塞
park()方法响应中断。线程被中断时会从park()中返回,但不会抛出InterruptedException。需要自己检查中断状态。
import java.util.concurrent.locks.LockSupport;
public class InterruptibleParkDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("工作线程:开始park,等待中断...");
LockSupport.park();
// 被唤醒或中断后,检查中断标志
if (Thread.currentThread().isInterrupted()) {
System.out.println("工作线程:是被中断唤醒的,处理中断逻辑。");
// 根据需要决定是否清理中断标志
} else {
System.out.println("工作线程:是被正常unpark唤醒的。");
}
});
worker.start();
Thread.sleep(1000);
System.out.println("主线程:中断工作线程");
// 中断worker线程,这会导致其park()返回
worker.interrupt();
}
}
步骤5:实现带超时的阻塞
使用parkNanos或parkUntil实现等待超时。
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.TimeUnit;
public class ParkWithTimeoutDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long start = System.currentTimeMillis();
System.out.println("工作线程:开始park,最多等待2秒...");
// park最多2秒(转换为纳秒)
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
long elapsed = System.currentTimeMillis() - start;
System.out.println("工作线程:park返回,实际等待约" + elapsed + "ms。");
});
worker.start();
// 主线程不调用unpark,让工作线程等待超时
Thread.sleep(3000);
}
}
执行结果:工作线程等待约2秒后自动从parkNanos返回。
五、高级应用场景与模式
场景1:构建简单的锁(自定义互斥锁)
LockSupport是实现锁(如ReentrantLock)的底层关键。下面是一个极简示例:
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
public class SimpleLock {
private final AtomicBoolean locked = new AtomicBoolean(false);
private Thread owner = null; // 持有锁的线程
public void lock() {
// 自旋尝试获取锁
while (!locked.compareAndSet(false, true)) {
// 获取失败,阻塞自己,等待被唤醒后重试
LockSupport.park();
}
owner = Thread.currentThread();
System.out.println(owner.getName() + " 获得了锁。");
}
public void unlock() {
if (Thread.currentThread() != owner) {
throw new IllegalMonitorStateException();
}
owner = null;
locked.set(false);
// 唤醒一个可能正在等待获取锁的线程
// 实际上,这里唤醒的是哪个线程是不确定的,因为park在while循环中
// 更复杂的实现需要维护一个等待队列(如AQS)
Thread waiter = ...; // 需要一个机制找到等待者,这里仅示意
if (waiter != null) {
LockSupport.unpark(waiter);
}
System.out.println(Thread.currentThread().getName() + " 释放了锁。");
}
}
注意:这是一个高度简化的模型,真实锁实现(如AQS)远比这复杂,需要精确管理等待队列和公平性。
场景2:实现生产者-消费者中的唤醒
利用LockSupport的精确唤醒特性,可以设计更高效的生产消费模式。
import java.util.Queue;
import java.util.LinkedList;
import java.util.concurrent.locks.LockSupport;
public class ParkBasedProducerConsumer {
private final Queue<String> queue = new LinkedList<>();
private Thread consumerThread;
public void startConsumer() {
consumerThread = new Thread(() -> {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
// 暂时释放锁,并在队列上等待(这里用wait更合适,但为演示park)
// 实际上,在监视器锁内部使用park是危险的,可能导致锁无法释放
// 此处仅为逻辑示意,建议在锁外部使用park
queue.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
String item = queue.poll();
System.out.println("消费: " + item);
}
}
});
consumerThread.start();
}
public void produce(String item) {
synchronized (queue) {
queue.add(item);
System.out.println("生产: " + item);
// 唤醒消费者线程。由于我们在锁内操作,这里用notify更合适。
// 如果用park/unpark,需要在锁外进行,设计更复杂。
LockSupport.unpark(consumerThread);
}
}
}
重要提示:在典型的监视器锁(synchronized)内部使用LockSupport需要非常小心,容易造成死锁或程序行为异常。通常,LockSupport与基于它构建的高级同步器(如Condition、CountDownLatch)结合使用。
六、使用注意事项
- 许可的单一性:许可是每个线程独有的布尔值,不是累加的。连续调用两次
unpark和一次unpark的效果相同(许可仍为true)。一次park消耗一次许可。 - 中断处理:
park()因中断返回后,线程的中断标志保持设置状态。需要根据业务逻辑决定是抛出异常、清理退出还是继续执行。 - 避免在
synchronized块内长期park:如果线程在持有监视器锁(synchronized)时park,其他线程将无法获取该锁去unpark它,可能导致死锁。LockSupport.park()通常用于Lock接口的实现内部或独立的阻塞场景。 - 调试:被
park阻塞的线程,在调试工具或jstack中会显示为WAITING (parking)状态。 park可能虚假唤醒:在没有明确unpark、中断或超时的情况下,park也可能返回。因此,必须在循环条件中检查是否真的满足继续执行的条件(while(!condition) park();)。

暂无评论,快来抢沙发吧!