为什么有wait和notify方法还要引入LockSupport?

wait和notify的运行逻辑

写在前面

  1. Entry Set(锁池):未获取到 monitor 锁、正在竞争锁的线程,线程状态为 BLOCKED。
  2. 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操作?

不行!

  1. 只要利用 synchronized 关键字做同步,必须配合 wait/notify。LockSupport 必须配合 Lock 接口(如 ReentrantLock)使用,它不认识对象监视器锁
  2. LockSupport.unpark(Thread t):是面向线程的,必须明确知道我接下来unpark哪个线程;obj.notify():是面向对象监视器的,不需要知道谁在等,只是通知一声这个监视器锁可以使用了就行,由JVM在entry set中挑一个唤醒
  3. 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

Java并发编程之LockSupport

JUC锁: LockSupport详解

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐