LockSupport工具类

151 阅读9分钟

LockSupport

是什么?

官方说明:www.apiref.com/java11-zh/j…

LockSupport是一个工具类,提供了基本的线程阻塞和唤醒功能,它是创建锁和其他同步组件的基础工具,内部是使用sun.misc.Unsafe类实现的。

LockSupport和使用它的线程都会关联一个许可,park方法表示消耗一个许可,调用park方法时,如果许可可用则park方法返回,如果没有许可则一直阻塞直到许可可用。unpark方法表示增加一个许可,多次调用并不会积累许可,因为许可数最大值为1。

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

  • LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认是0。可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。

LockSupport底层是UNSAFE。

  • permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。

  • 调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。

  • LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,
    调用一次unpark就将0变成1,调用一次park会消费permit,也就是将1变成o,同时park立即返回。如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。

  • 形象的理解
    线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。

    • 当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;如果无凭证,就必须阻塞等待凭证可用;

    • 而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。

类图

主要方法介绍

  • park(): 阻塞当前线程,直到unpark方法被调用或当前线程被中断,park方法才会返回。

  • park(Object blocker): 同park()方法,多了一个阻塞对象blocker参数。

  • 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参数。

  • unpark(Thread thread): 唤醒处于阻塞状态的线程thread。

源码分析

类的构造函数

// 私有构造函数,无法被实例化
private LockSupport() {}

类的属性

public class LockSupport {
    // Hotspot implementation via intrinsics API
    private static final sun.misc.Unsafe UNSAFE;
    // 表示内存偏移地址
    private static final long parkBlockerOffset;
    // 表示内存偏移地址
    private static final long SEED;
    // 表示内存偏移地址
    private static final long PROBE;
    // 表示内存偏移地址
    private static final long SECONDARY;
    
    static {
        try {
            // 获取Unsafe实例
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            // 线程类类型
            Class<?> tk = Thread.class;
            // 获取Thread的parkBlocker字段的内存偏移地址
            parkBlockerOffset = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("parkBlocker"));
            // 获取Thread的threadLocalRandomSeed字段的内存偏移地址
            SEED = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSeed"));
            // 获取Thread的threadLocalRandomProbe字段的内存偏移地址
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
            // 获取Thread的threadLocalRandomSecondarySeed字段的内存偏移地址
            SECONDARY = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception ex) { throw new Error(ex); }
    }
}

类的主要方法

static void park()  //挂起当前线程,等待一个许可 
static void	unpark(Thread thread)	//为某个线程提供一个许可,唤醒某个指定线程

在分析LockSupport函数之前,先引入sun.misc.Unsafe类中的park和unpark函数,因为LockSupport的核心函数都是基于Unsafe类中定义的park和unpark函数,

下面给出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() {
        // 获取许可,设置时间为无限长,直到可以获取许可
        UNSAFE.park(false, 0L);
    }

调用了park函数后,会禁用当前线程,除非许可可用。在以下三种情况之一发生之前,当前线程都将处于休眠状态,即下列情况发生时,当前线程会获取许可,可以继续运行。

  • 其他某个线程将当前线程作为目标调用 unpark。
  • 其他某个线程中断当前线程。
  • 该调用不合逻辑地(即毫无理由地)返回。

unpark函数

此函数表示如果给定线程的许可尚不可用,则使其可用。如果线程在 park 上受阻塞,则它将解除其阻塞状态。否则,保证下一次调用 park 不会受阻塞。如果给定线程尚未启动,则无法保证此操作有任何效果。具体函数如下:

public static void unpark(Thread thread) {
    if (thread != null) // 线程为不空
        UNSAFE.unpark(thread); // 释放该线程许可
}

3种让线程等待和唤醒的方法

  • 方式1:  使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程

  • 方式2:  使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程

  • 方式3:  LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

方式1:  使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程

class MyThread extends Thread {

    public void run() {
        synchronized (this) {
            System.out.println("before notify");
            notifyAll();
            System.out.println("after notify");
        }
    }
}

public class WaitAndNotifyDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        // 主线程在第一次启动的时候肯定会获取到锁
        synchronized (t1) {
            try {
                //t1线程启动,
                t1.start();
                System.out.println("before wait");
                // 主线程睡眠3s
                TimeUnit.SECONDS.sleep(3);
                // 阻塞主线程
                t1.wait();
                System.out.println("after wait");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

结果

before wait
before notify
after notify
after wait

wait():作用是使当前线程从调用处中断并且释放锁转入等待队列,直到收到notify或者notifyAll的通知才能从等待队列转入锁池队列,没有收到停止会一直死等。

正常情况下

public class LockSupportDemo1 {
    static Object objectLock = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (objectLock){
                System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
            }
        },"A").start();

        new Thread(() -> {
            synchronized (objectLock)
            {
                objectLock.notify();
                System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
            }
        },"B").start();
    }

}

结果:

A	------come in
B	------通知
A	------被唤醒

Process finished with exit code 0

异常情况1

去掉同步代码块

public class LockSupportDemo1 {
    static Object objectLock = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
//            synchronized (objectLock){
                System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
//            }
        },"A").start();

        new Thread(() -> {
//            synchronized (objectLock){
                objectLock.notify();
                System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
//            }
        },"B").start();
    }
}

结果:

A	------come in
Exception in thread "A" Exception in thread "B" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.youth.guiguthirdquarter.AQS.LockSupportDemo1.lambda$main$0(LockSupportDemo1.java:16)
	at java.lang.Thread.run(Thread.java:748)
java.lang.IllegalMonitorStateException
	at java.lang.Object.notify(Native Method)
	at com.youth.guiguthirdquarter.AQS.LockSupportDemo1.lambda$main$1(LockSupportDemo1.java:26)
	at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0

报错。

异常情况2

先唤醒,再等待。

public class LockSupportDemo1 {
    static Object objectLock = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (objectLock){
                System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
            }
        },"A").start();

        new Thread(() -> {
            synchronized (objectLock)
            {
                objectLock.notify();
                System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
            }
        },"B").start();
    }
}

结果:

B	------通知
A	------come in

Process finished with exit code -1

死循环,A无法被唤醒了。

这两点我们之前也说过,Object类提供的wait和notify

  1. 只能在synchronized同步代码块里使用

  2. 只能先等待(wait),再唤醒(notify)。顺序一旦错了,那个等待线程就无法被唤醒了。

方式2:  使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程

缺点和Object类里的wait,notify一样。

  1. 只能在lock同步代码块里使用,不然就报错
  2. 只能先等待(await),再唤醒(signal)。顺序一旦错了,那个等待线程就无法被唤醒了。

但相对于wait,notify改进的一点是,可以绑定lock进行定向唤醒。

方式3:  LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

有的时候我不需要进入同步代码块,只是需要让线程阻塞,这个时候LockSupport就发挥作用了。并且还解决了之前的第二个问题,也就是等待必须在唤醒的前面。

static void park()  //挂起当前线程,等待一个许可 
static void	unpark(Thread thread)	//为某个线程提供一个许可,唤醒某个指定线程

异常情况1

无同步代码块

public class LockSupportDemo3 {
    public static void main(String[] args) {
        /**
         LockSupport:俗称 锁中断
         LockSupport它的解决的痛点
         1。LockSupport不用持有锁块,不用加锁,程序性能好,
         2。不需要等待和唤醒的先后顺序,不容易导致卡死
         */
        Thread t1 = new Thread(() -> {

            System.out.println(Thread.currentThread().getName() + "\t ----begin-时间:" + System.currentTimeMillis());
            LockSupport.park();//阻塞当前线程
            System.out.println(Thread.currentThread().getName() + "\t ----被唤醒-时间:" + System.currentTimeMillis());
        }, "t1");
        t1.start();
        LockSupport.unpark(t1);
        System.out.println(Thread.currentThread().getName() + "\t 通知t1...");


    }
}

结果:

t1	 ----begin-时间:1603376148147
t1	 ----被唤醒-时间:1603376148147
main	 通知t1...

Process finished with exit code 0

没有问题

异常情况2

先唤醒,再阻塞(等待)

public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t ----begin-时间:" + System.currentTimeMillis());
            LockSupport.park();//阻塞当前线程
            System.out.println(Thread.currentThread().getName() + "\t ----被唤醒-时间:" + System.currentTimeMillis());
        }, "t1");
        t1.start();
        LockSupport.unpark(t1);
        System.out.println(Thread.currentThread().getName() + "\t 通知t1...");


    }

结果:

main	 通知t1...
t1	 ----begin-时间:1603376257183
t1	 ----被唤醒-时间:1603376257183

Process finished with exit code 0

可以看到,如果你先唤醒了。那么后面的LockSupport.park();就相当于瞬间被唤醒了,不会和之前一样程序卡死。为什么呢?结合之前分析的流程

  1. 先执行unpark,将许可证由0变为1

  2. 然后park来了发现许可证此时为0(也就是有许可证),那么他就不会阻塞,马上就往后执行。同时消耗许可证(也就是将1又变为0)。