1、 LockSupport是什么

LockSupport是java.util.concurrent.locks包下的一个类。
官方解释:LockSupport是用于创建锁和其他同步类的基本线程阻塞原语。
—-> 可以理解为,是原本的线程等待唤醒机制wait()/notify()的加强版。
LockSupport下的两个重要方法:

  • park():用于阻塞线程;
  • unpark():用于解除阻塞线程;

2、使用LockSupport进行线程间通信

之前,我们使用synchronized关键字时,可以通过wait()/notify()/notifyAll()实现线程间通信;使用Lock锁,可以通过Condition对象的await()/signal()/signalAll()实现线程间的通信。
现在,突然冒出来一个LockSupprot,也是实现线程间通信,那么为什么要用LockSuppot呢?它是线程等待唤醒机制wait()/notify()的改良版,功能更强大。

2.1 三种线程等待和唤醒的方法(线程间通信方法)

  • 方式一:使用Object类中的wait()方法让线程等待,使用Object类中的notify()/notifyAll方法唤醒线程(要在synchronized方法或者sunchronized代码块中使用);
  • 方式二:使用JUC包中Condition接口实现类的await()方法()让线程等待,使用signal()方法或者signalAll()方法唤醒线程,一般是配合Lock接口的子类使用;
  • 方式三:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程。

那么,既然已经有方式,进行线程间等到与唤醒了,为什么还需要LockSupport()呢?先来看下之前的方式的一些缺陷:

  • wait()/notify()的缺陷:
    • wait()/notify()方法必须在synchronized方法或者syunchronized代码块中调用运行,否则会出现IllegalMonitorStateException异常,非法的监视器状态异常;
    • notify()方法进行线程唤醒,只能唤醒在调用notify()方法之前进行了wait()的线程。即要先等待,后唤醒;
    • notify()唤醒一个线程,唤醒的线程是随机的。
  • await()/signal()的缺陷:
    • await()/signal()必须在锁块,Lock下的lock()方法和unlock()方法中调用运行,否则会出现IllEgalMonitorStateException异常,非法的监视器状态异常;
    • signal()方法进行线程唤醒,只能唤醒在调用signal()方法之前进行了await()的线程。即要先等待,后唤醒。
    • signal()唤醒一个线程,唤醒的线程是随机的。

既然前两种方式有这些缺陷,而LockSupport是改良版,功能更强大,那么LockSupport进行线程间的通信又是怎样呢?
LockSupport使用了一种名为permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个permit(许可),permit只有两个值:0和1,默认是0,0代表没有许可,也就是说,许可默认是被占用的。可以把permit看作是一种信号量(Seemaphore),但是和Semaophore不同的是,许可的累加上限是1.
先来看下LockSupprot中的方法,LockSupprot是一个工具类,不支持构造,提供了一堆static方法。

2.2 LockSupport方法介绍

LockSupprot的重要方法是park()和unpark(Thread thread)方法,通过他们来实现阻塞和唤醒线程的操作。

  • park():

将当前调用线程阻塞

  1. public static void park() {
  2. UNSAFE.park(false, 0L);
  3. }
  • unpark(Thread thread):

将指定线程唤醒

  1. public static void unpark(Thread thread) {
  2. if (thread != null)
  3. UNSAFE.unpark(thread);
  4. }

可以看到,park()、unpark()底层欧式调用UNSAFE来实现的,UNSAFE后面学习。

2.3 LockSupport使用示例

先看一个先park()进行阻塞,然后unpark()进行唤醒的案例:

  1. package com.yuanhai.demo;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.locks.LockSupport;
  4. /**
  5. * 本类说明:
  6. * LockSupport 使用示例
  7. * @author yuanhai
  8. * @date 2022年05月22日
  9. */
  10. public class LockSupportDemo {
  11. public static void main(String[] args) {
  12. Thread threadA = new Thread(() -> {
  13. System.out.println(Thread.currentThread().getName()+"\t"+"---come in");
  14. LockSupport.park(); // 被阻塞,等待通知等待放行,需要通过permit
  15. System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒");
  16. }, "A");
  17. threadA.start();
  18. // 暂停线程几秒钟
  19. try {
  20. TimeUnit.SECONDS.sleep(3L);
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. Thread threadB = new Thread(() -> {
  25. LockSupport.unpark(threadA);
  26. System.out.println(Thread.currentThread().getName()+"\t"+"---通知了");
  27. }, "B");
  28. threadB.start();
  29. }
  30. }

运行结果:

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

更改一下这个案例,先通知,再唤醒:

  1. package com.yuanhai.demo;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.locks.LockSupport;
  4. /**
  5. * 本类说明:
  6. * LockSupport 使用示例
  7. * @author yuanhai
  8. * @date 2022年05月22日
  9. */
  10. public class LockSupportDemo {
  11. public static void main(String[] args) {
  12. Thread threadA = new Thread(() -> {
  13. // 暂停线程几秒钟,实现让B线程先进行通知
  14. try {
  15. TimeUnit.SECONDS.sleep(3L);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. System.out.println(Thread.currentThread().getName()+"\t"+"---come in"+System.currentTimeMillis());
  20. LockSupport.park(); // 被阻塞,等待通知等待放行,需要通过permit
  21. System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒"+System.currentTimeMillis());
  22. }, "A");
  23. threadA.start();
  24. Thread threadB = new Thread(() -> {
  25. LockSupport.unpark(threadA);
  26. System.out.println(Thread.currentThread().getName()+"\t"+"---通知了");
  27. }, "B");
  28. threadB.start();
  29. }
  30. }

运行结果:

B —-通知了 A —-come in1653205417579 A —-被唤醒1653205417579

可以看到,先通知,再阻塞,线程A还是被唤醒了。同时,从打印出的毫秒数可以看出,线程A中的LockSupport.park()根本没有起作用。 (这是:提前发放通行证

2.4 LockSupprot细节说明

LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。在LockSupport的底层,是调用Unsafe类中的native代码来实现的。
park()和unpark()方法阻塞线程和解除线程阻塞的过程:

  1. LockSupprot和每个使用它的线程都有一个permit(许可)关联。permit只有两种值,1和0,相当于一个开关,默认是0,为0时就是无许可,处于阻塞状态;
  2. 调用一侧unpark()方法,permit就加1,变成1;
  3. 调用一次park会消费permit,也就是将permit从1变成0,同时park()立即返回;
  4. 如果再次调用park(),线程会变成阻塞,这时调用unpark()会把permit置为1
  5. 每个线程都有一个相关的permit,且permit只有一个,重复调用unpark()也不会积累凭证(这也说明,LockSupport是不可重入的,如果一个线程连续调用LockSupprot.park()两次,那么线程一定会一直阻塞下去)。

形象些理解:
线程阻塞需要消耗凭证,这个凭证最多只有一个。
调用park()方法时:

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

而unpark()则相反,它会增加一个凭证,但是凭证最多只有一个,不会累加。
这样就可以解释如下两个问题:
为什么LockSupprot可以先唤醒线程,后阻塞线程?
因为unpark()获得了一个凭证,之后再调用park()方法,可以直接消耗掉这个凭证,所以可以先唤醒。
为什么LockSupprot唤醒两次后,阻塞两次,最终结果还是会阻塞线程?
因为凭证最多为1,不可累加。唤醒两次permit都只是1,两次park需要两次凭证。

3、LockSupport底层原理

后续学习。主要是Unsafe类调用底层的native方法。