为什么有wait和notify方法还要引入LockSupport
写在前面一般的状态变更逻辑o.wait。
为什么有wait和notify方法还要引入LockSupport?
wait和notify的运行逻辑
写在前面
- Entry Set(锁池):未获取到 monitor 锁、正在竞争锁的线程,线程状态为 BLOCKED。
- Wait Set(等待池):已获取过锁但因条件不满足,调用 wait() 主动释放锁并等待通知的线程,线程状态为 WAITING。
一般的状态变更逻辑
- wait set → entry set:收到 notify / notifyAll,准备重新竞争锁
- entry set → runnable:成功获取 monitor 锁
- runnable → wait set:在持有锁的情况下调用 wait()
- wait 和 notify 必须在
synchronized代码块使用 - 当线程A调用
o.wait方法时,A先释放锁,再把A加入到o的wait set中,最后A进入WAITING态 - 当线程调用o.notify方法时,就一个动作:把o的wait set中的某线程,移动o的entry set中
- 除此之外,entry set中的线程由JVM调度拿锁
先notify导致的通知丢失问题
Object lock = new Object();
// 线程 1
synchronized(lock) {
lock.wait(); // 如果 notify 在这之前执行,就浪费掉了此次notify信号
// do something
}
// 线程 2
synchronized(lock) {
lock.notify();
}
- 考虑这样一种情况,线程1没抢到锁,线程1进入到lock的entry set中
- 线程2抢到lock进入临界区,执行notify后,
- 把lock的wait set中某个线程转入到entry set中,但此时 wait set 为空,notify 什么都没唤醒(直接浪费)
- 线程2结束
- 线程1抢到lock进入临界区,执行wait后
- 释放lock
- 线程1从entry set 转到 wait set中
- 再也没有人 notify → 永久等待
- JVM再从lock的entry set中拿线程,但这时候已经没有线程了。此时锁空闲,线程1没有利用到notify信号,出现并发问题
因此使用wait/notify实现同步时,必须先调用wait,后调用notify,如果先调用notify,再调用wait,将起不了作用
直接解决
boolean hasNotified = false; // 引入一个状态变量
// 线程 1
synchronized(lock) {
while (!hasNotified) { // 如果已经被通知过,就不 wait 了
lock.wait();
}
// do something
}
// 线程 2
synchronized(lock) {
hasNotified = true; // 改变状态
lock.notify();
}
引入LockSupport
写在前面
- LockSupport不需要必须在
synchronized代码块中使用,可以在任何地方使用,不需要持有任何对象的锁- unpark 可以先于 park 调用,不会丢信号,不怕上述
先唤醒后等待的情况LockSupport.unpark(null)在语义上是合法的空操作,会直接返回
- LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。简而言之,当调用LockSupport.park时,表示当前线程将会等待,直至获得permit,当调用LockSupport.unpark时,必须把某个等待permit的线程 作为参数进行传递,好让此线程继续运行。
核心函数
在分析LockSupport函数之前,先引入sun.misc.Unsafe类中的park和unpark函数,因为LockSupport的核心函数都是基于Unsafe类中定义的park和unpark函数,下面给出两个函数的定义:
public native void park(boolean isAbsolute, long time);
public native void unpark(Thread thread);
- park函数,阻塞线程,并且该线程在下列情况发生之前都会被阻塞: ① 调用unpark函数,释放该线程的许可。② 该线程被中断。③ 设置的时间到了。并且,当time为绝对时间时,isAbsolute为true,否则,isAbsolute为false。当time为0时,表示无限等待,直到unpark发生。
- unpark函数,释放线程的许可,即激活调用park后阻塞的线程。
核心函数重载
park函数有两个重载版本,方法摘要如下
public static void park();
public static void park(Object blocker);
park(Object)型函数如下。
public static void park(Object blocker) {
// 获取当前线程
Thread t = Thread.currentThread();
// 设置Blocker
setBlocker(t, blocker);
try {
// 获取许可
UNSAFE.park(false, 0L);
} finally {
// 重新可运行后再此设置Blocker 防止内存泄露
setBlocker(t, null);
}
}
- 注意要运行两次setBlocker,第二次目的是,当恢复运行时避免内存泄露问题,及时置空(类似ThreadLocal中的及时remove逻辑)
底层原理:permit
基于二元信号量permit(只有0和1)的机制
-
park()
-
尝试扣减permit
-
如果当前有permit,则扣减掉,并继续执行,不会阻塞
-
如果当前permit=0,线程阻塞,进入WAITING态,直到有人发permit
-
-
unpark(Thread t)
- 把t的permit置为1
如果之前t的permit为0,则被唤醒
如果t还没运行到park方法,那么到它改调用park的时候,会直接通过,并消耗掉permit,permit置为0
permit最多就是1,如果对某一线程连续调用十次unpark,它的permit也是1(LockSupport 的 permit 是 一次性累积的)
Thread t1 = new Thread(() -> {
LockSupport.park(); // 哪怕 unpark 早就执行过了,这里也不会卡死,只会消耗掉许可直接通过
});
// 即使 unpark 在 start 之前执行也没问题,解决了通知丢失问题
LockSupport.unpark(t1);
t1.start();
其他方法
parkNanos(long nanos): 同park方法,nanos表示最长阻塞超时时间,超时后park方法将自动返回。
parkNanos(Object blocker, long nanos): 同parkNanos(long nanos)方法,多了一个阻塞对象blocker参数。
parkUntil(long deadline): 同park()方法,deadline参数表示最长阻塞到某一个时间点,当到达这个时间点,park方法将自动返回。(该时间为从1970年到现在某一个时间点的毫秒数)
parkUntil(Object blocker, long deadline): 同parkUntil(long deadline)方法,多了一个阻塞对象blocker参数。
阻塞对象blocker的作用
纯诊断用途的参数
通过前面方法介绍可以看到,park、parkNanos、parkUntil方法都有对应的带阻塞对象blocker参数的重载方法。
Thread类有一个变量为parkBlocker,对应的就是LockSupport的park等方法设置进去的标记对象,记录一下线程在等谁。
/**
* The argument supplied to the current call to
* java.util.concurrent.locks.LockSupport.park.
* Set by (private) java.util.concurrent.locks.LockSupport.setBlocker
* Accessed using java.util.concurrent.locks.LockSupport.getBlocker
*/
private volatile Object parkBlocker;
park() 和 park(this)
@Test
public void test(){
LockSupport.park();
}
@Test
public void test_2(){
LockSupport.park(this);
}
"main" #1 [9628] prio=5 os_prio=0 cpu=328.12ms elapsed=383.99s tid=0x00000224f7ae7e30 nid=9628 waiting on condition [0x00000069b1afe000]
java.lang.Thread.State: WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@21.0.5/Native Method)
at java.util.concurrent.locks.LockSupport.park(java.base@21.0.5/LockSupport.java:371)
at jstack.test(jstack.java:11)
"main" #1 [31008] prio=5 os_prio=0 cpu=281.25ms elapsed=30.42s tid=0x0000019e07080630 nid=31008 waiting on condition [0x0000004618ffe000]
java.lang.Thread.State: WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@21.0.5/Native Method)
- parking to wait for <0x0000000697c356a8> (a jstack)
at java.util.concurrent.locks.LockSupport.park(java.base@21.0.5/LockSupport.java:221)
中断响应
import java.util.concurrent.locks.LockSupport;
public class ParkInterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("线程准备 park");
LockSupport.park();
System.out.println("线程被唤醒,isInterrupted = "+ Thread.currentThread().isInterrupted());
});
t.start();
Thread.sleep(1000);
System.out.println("主线程 interrupt 子线程");
t.interrupt();
}
}
线程准备 park
主线程 interrupt 子线程
线程被唤醒,isInterrupted = true
- park方法响应中断,interrupt起到的作用与unpark一样
- 中断标志仍然是 true
因此要及时处理并发现中断
class Worker extends Thread {
@Override
public void run() {
while (true) {
System.out.println("等待任务...");
LockSupport.park();
if (Thread.currentThread().isInterrupted()) {
System.out.println("收到中断,安全退出");
break;
}
System.out.println("执行任务");
}
}
}
与wait操作不同的中断处理方式
| 行为 | Object.wait() |
LockSupport.park() |
|---|---|---|
| 被 interrupt | ❌ 抛 InterruptedException |
✅ 直接返回 |
| 中断标志 | ❌ 被清除 | ✅ 保留 |
| 必须在 synchronized | ✅ | ❌ |
是不是LockSupport可以代替所有的wait+notify操作?
不行!
- 只要利用 synchronized 关键字做同步,必须配合 wait/notify。LockSupport 必须配合 Lock 接口(如 ReentrantLock)使用,它不认识对象监视器锁
- LockSupport.unpark(Thread t):是面向线程的,必须明确知道我接下来unpark哪个线程;obj.notify():是面向对象监视器的,不需要知道谁在等,只是通知一声这个监视器锁可以使用了就行,由JVM在entry set中挑一个唤醒
- notifyAll支持一下子广播唤醒,LockSupport只能一个一个unpark
什么东西可以替代所有的wait+notify操作?
| 特性 | 原始版 (synchronized) | 现代版 (Lock + Condition) |
|---|---|---|
| 互斥锁 | synchronized(obj) | lock.lock() |
| 等待 | obj.wait() | condition.await() |
| 唤醒 | obj.notify() | condition.signal() |
| 释放锁? | 是 (自动) | 是 (自动) |
LockSupport面试题
有 3 个独立的线程,一个只会输出 A,一个只会输出 B,一个只会输出 C,在三个线程启动的情况下,请用合理的方式让他们按顺序打印 ABCABC
public class ABCABC {
private static MyThread t1, t2, t3;
@Test
public void test() throws InterruptedException {
t1 = new MyThread("A");
t2 = new MyThread("B");
t3 = new MyThread("C");
t1.nextThread = t2;
t2.nextThread = t3;
t3.nextThread = t1;
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
Thread.sleep(10000);
}
static class MyThread extends Thread {
String print;
MyThread nextThread;
public MyThread(String s) {
this.print = s;
}
@Override
public void run() {
while (true) {
LockSupport.park();
System.out.println(print);
LockSupport.unpark(nextThread);
}
}
}
}
Ref
更多推荐


所有评论(0)