Day44 | J.U.C中的LockSupport详解
在之前的文章中,我们已经熟悉了Java并发包中的原子类。今天,我们将继续深入J.U.C的核心,探讨一个看着不太起眼但实际却很重要的底层工具——LockSupport。
LockSupport是java.util.concurrent包里线程阻塞和唤醒机制的基础。
如果我们把ReentrantLock、Semaphore等高级同步工具看成精密仪器,那LockSupport就是制造这些仪器所依赖的最核心的零件之一。
理解他,便于理解我们之后将要学习的AQS (AbstractQueuedSynchronizer)。
一、为什么需要LockSupport?
在LockSupport出现之前,我们有两种主要的方式来让线程等待和唤醒:
1、使用Thread.suspend()和Thread.resume()
这对方法已经被废弃。因为太容易导致死锁。如果一个持有锁的线程被suspend(),他就永远不会释放锁,导致其他等待这个锁的线程无限期地阻塞。
2、使用Object.wait()和Object.notify()/notifyAll()
这是最经典和常用的方式。但是这种方式也有一定程度的限制。
比如必须在synchronized块里调用,wait()和notify()必须跟对象的监视器锁绑定,这就意味着线程必须先获取锁,才能进行等待或通知操作,不够灵活。
还会存在信号丢失的问题,比如一个线程先于另一个线程调用notify(),之后另一个线程才调用wait(),那么这个通知信号就丢失了,wait()的线程会无限等待下去。
二、LockSupport的核心机制
LockSupport给每个线程都关联了一个许可(Permit)。这个许可像一张通行证,他是一个二进制的信号量(要么有,要么没有)。
LockSupport最核心的两个方法就是围绕这个许可工作的:
park方法
public static void park() {U.park(false, 0L);}
如果当前线程持有许可,那么他会消耗掉这个许可,并立即返回,线程继续运行。
如果当前线程没有许可,那么他就会阻塞,直到有其他线程给他发放许可。
unpark方法
public static void unpark(Thread thread) {if (thread != null)U.unpark(thread);}
给一个指定的线程发放一个许可。
如果thread当前正因为park()而阻塞,他会被立刻唤醒。
如果thread当前没有阻塞,那么这次发放的许可就会被攒起来(不是累加,最多一个)。当这个线程下一次调用park()的时候,他会立刻消耗掉这个许可,从而避免阻塞。
unpark()可以先于park()调用,并且许可不会丢失。这就解决了wait/notify的信号丢失问题。
当我们打开LockSupport的源码,其实会发现代码量并不多:
其中重要的就是
添加图片注释,不超过 140 字(可选)
Unsafe被称之为Java的上帝之手,是因为他可以执行内存操作等一些底层操作。
parkBlocker是Thread类的一个字段,用来调试和监控,记录当前线程是被哪个对象所阻塞的。LockSupport.park(Object blocker)就会设置这个字段。
UNSAFE.park()和UNSAFE.unpark()这两个native方法,实现都在JVM内部(C/C++编写),最终会调用操作系统的线程调度原语,实现真正意义上的线程挂起和恢复。
三、LockSupport实战
概念和术语通常都会让人摸不着头脑,惯例还是直接上手写一下,接下来我们看一下两个LockSupport的代码示例:
1、park与unpark方法的使用
package com.lazy.snail.day44;import java.util.concurrent.locks.LockSupport;/*** @ClassName LockSupportDemo1* @Description TODO* @Author lazysnail* @Date 2025/9/1 13:56* @Version 1.0*/
public class LockSupportDemo1 {public static void main(String[] args) throws InterruptedException {Thread worker = new Thread(() -> {System.out.println(Thread.currentThread().getName() + ": 即将调用park()进入等待...");// 这里线程还没有许可,线程会阻塞LockSupport.park();System.out.println(Thread.currentThread().getName() + ": 已被unpark,继续执行。");}, "工人线程");worker.start();System.out.println("主线程休眠2秒,准备为工人线程发放许可...");Thread.sleep(2000);System.out.println("主线程调用unpark() ...");// 给worker线程发放一个许可LockSupport.unpark(worker);}
}
示例代码中主线程创建并启动了worker线程。
worker打印输出后,执行LockSupport.park()。由于没有许可,worker阻塞,线程状态变成WAITING。
主线程sleep(2000),保证worker已经真的阻塞在park()。
然后主线程调用LockSupport.unpark(worker),给worker发放了一个许可,如果线程正阻塞在park(),会被唤醒并消费掉许可(许可回到0)。
worker从park()返回,打印输出后线程结束。
这段代码演示的就是LockSupport最核心的一次性许可机制,park()消费许可进入/退出等待,unpark(thread)给指定线程发放最多1个许可。
2、unpark先于park执行
之前我们说wait/notify会有信号丢失的问题,通过下面的案例来看看LockSupport存不存在这个问题。
package com.lazy.snail.day44;import java.util.concurrent.locks.LockSupport;/*** @ClassName LockSupportDemo2* @Description TODO* @Author lazysnail* @Date 2025/9/1 14:43* @Version 1.0*/
public class LockSupportDemo2 {public static void main(String[] args) throws InterruptedException {Thread worker = new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + ":等待2秒...");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":即将调用park()...");LockSupport.park();System.out.println(Thread.currentThread().getName() + ":park()执行完毕,没有阻塞。");}, "工人线程");worker.start();System.out.println("主线程提前给工人线程发放许可...");LockSupport.unpark(worker);}
}
示例代码中主线程启动worker,然后马上unpark(worker),许可被置成了1。
worker线程sleep(2s)醒来后调用park(),发现许可是1,马上返回并消费。
worker线程继续执行并结束,全程不阻塞。
这个示例说明了先unpark,后park也不会阻塞。
3、park()对线程中断的响应
当一个被park()阻塞的线程被interrupt()时,park()方法会立即返回,并且该线程的中断状态会被设置为true。
package com.lazy.snail.day44;import java.util.concurrent.locks.LockSupport;/*** @ClassName LockSupportDemo3* @Description TODO* @Author lazysnail* @Date 2025/9/1 15:59* @Version 1.0*/
public class LockSupportDemo3 {public static void main(String[] args) throws InterruptedException {Thread worker = new Thread(() -> {System.out.println("工人线程:即将park...");LockSupport.park();System.out.println("工人线程:已被唤醒。");System.out.println("工人线程中断状态:" + Thread.currentThread().isInterrupted());});worker.start();Thread.sleep(2000);System.out.println("主线程:中断工人线程...");worker.interrupt();}
}
示例代码中,worker线程被阻塞,然后在主线程中被中断,park方法就马上返回了,中断状态变成了true。
四、核心属性及方法
核心属性
private static final Unsafe U = Unsafe.getUnsafe();private static final long PARKBLOCKER= U.objectFieldOffset(Thread.class, "parkBlocker");private static final long TID= U.objectFieldOffset(Thread.class, "tid");
U:Unsafe类的一个实例,LockSupport使用他来调用底层的原生park和unpark函数。
PARKBLOCKER:一个long类型的值,他保存了Thread对象内部parkBlocker字段的内存偏移量。parkBlocker是一个对象,可以跟一个线程关联,来表示他被阻塞的原因。
TID:也是一个long类型的值,存储了Thread对象内部tid(线程ID)字段的内存偏移量。这样LockSupport就可以直接从内存里拿到线程的ID。
核心方法
public static void unpark(Thread thread) {if (thread != null)U.unpark(thread);}public static void park() {U.park(false, 0L);}public static void park(Object blocker) {Thread t = Thread.currentThread();setBlocker(t, blocker);U.park(false, 0L);setBlocker(t, null);}
unpark(Thread thread)、park()和park(Object blocker)已经在第三节中做了介绍。
parkNanos(long nanos)方法是带超时的park(),表示最多阻塞nanos纳秒。
parkUntil(long deadline)方法是带截止日期的park(),表示最多阻塞到deadline这个绝对时间点。
getBlocker(Thread t)用来获取指定线程t的blocker对象,主要用于监控和诊断。
五、LockSupport和AQS的关系
LockSupport实际是AQS框架的线程调度引擎。
AQS内部维护了一个等待线程的队列。
当一个线程尝试获取锁失败后,AQS会把他包装成一个节点(Node)加入等待队列,然后调用 LockSupport.park(this)把这个线程安全地挂起。
当持有锁的线程释放锁时,AQS会从队列中找到需要被唤醒的后继节点,然后调用 LockSupport.unpark(node.thread) 来精确地唤醒那个等待的线程。
结语
通过阅读本文,以及前期讲的Thread.sleep(ms)、Object.wait()、Condition.await(),我把这些线程阻塞的方法列一个表,方便理解对别:
特性 / 方法 | Thread.sleep(ms) | Object.wait() | Condition.await() | LockSupport.park() |
---|---|---|---|---|
是否释放锁 | 不释放 | 释放synchronized锁 | 释放Lock锁 | 不释放 |
调用要求 | 无要求 | 必须在synchronized块中 | 必须持有Lock | 无要求 |
唤醒方式 | 时间到期 | notify/notifyAll | signal/signalAll | unpark/interrupt |
响应中断 | 抛出InterruptedException | 抛出InterruptedException | 抛出InterruptedException | 不抛异常,直接返回并设置中断状态 |
信号丢失问题 | 不适用 | 会丢失 (先notify后wait) | 会丢失 (先signal后await) | 不会丢失 (先unpark后park) |
底层实现 | JVM/OS | 对象监视器(monitor) | AQS + LockSupport.park | Unsafe + OS线程调度原语 |
下一篇预告
Day45 | J.U.C中AQS的完全指南(上)
如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!