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():
将当前调用线程阻塞
public static void park() {
UNSAFE.park(false, 0L);
}
- unpark(Thread thread):
将指定线程唤醒
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
可以看到,park()、unpark()底层欧式调用UNSAFE来实现的,UNSAFE后面学习。
2.3 LockSupport使用示例
先看一个先park()进行阻塞,然后unpark()进行唤醒的案例:
package com.yuanhai.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
* 本类说明:
* LockSupport 使用示例
* @author yuanhai
* @date 2022年05月22日
*/
public class LockSupportDemo {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t"+"---come in");
LockSupport.park(); // 被阻塞,等待通知等待放行,需要通过permit
System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒");
}, "A");
threadA.start();
// 暂停线程几秒钟
try {
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread threadB = new Thread(() -> {
LockSupport.unpark(threadA);
System.out.println(Thread.currentThread().getName()+"\t"+"---通知了");
}, "B");
threadB.start();
}
}
运行结果:
A —-come in B —-通知了 A —-被唤醒
更改一下这个案例,先通知,再唤醒:
package com.yuanhai.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
* 本类说明:
* LockSupport 使用示例
* @author yuanhai
* @date 2022年05月22日
*/
public class LockSupportDemo {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
// 暂停线程几秒钟,实现让B线程先进行通知
try {
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"---come in"+System.currentTimeMillis());
LockSupport.park(); // 被阻塞,等待通知等待放行,需要通过permit
System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒"+System.currentTimeMillis());
}, "A");
threadA.start();
Thread threadB = new Thread(() -> {
LockSupport.unpark(threadA);
System.out.println(Thread.currentThread().getName()+"\t"+"---通知了");
}, "B");
threadB.start();
}
}
运行结果:
B —-通知了 A —-come in1653205417579 A —-被唤醒1653205417579
可以看到,先通知,再阻塞,线程A还是被唤醒了。同时,从打印出的毫秒数可以看出,线程A中的LockSupport.park()根本没有起作用。 (这是:提前发放通行证)
2.4 LockSupprot细节说明
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。在LockSupport的底层,是调用Unsafe类中的native代码来实现的。
park()和unpark()方法阻塞线程和解除线程阻塞的过程:
- LockSupprot和每个使用它的线程都有一个permit(许可)关联。permit只有两种值,1和0,相当于一个开关,默认是0,为0时就是无许可,处于阻塞状态;
- 调用一侧unpark()方法,permit就加1,变成1;
- 调用一次park会消费permit,也就是将permit从1变成0,同时park()立即返回;
- 如果再次调用park(),线程会变成阻塞,这时调用unpark()会把permit置为1
- 每个线程都有一个相关的permit,且permit只有一个,重复调用unpark()也不会积累凭证(这也说明,LockSupport是不可重入的,如果一个线程连续调用LockSupprot.park()两次,那么线程一定会一直阻塞下去)。
形象些理解:
线程阻塞需要消耗凭证,这个凭证最多只有一个。
调用park()方法时:
- 如果有凭证,则会直接消耗掉这个凭证然后正常退出;
- 如果无凭证,就必须阻塞等待凭证可用;
而unpark()则相反,它会增加一个凭证,但是凭证最多只有一个,不会累加。
这样就可以解释如下两个问题:
为什么LockSupprot可以先唤醒线程,后阻塞线程?
因为unpark()获得了一个凭证,之后再调用park()方法,可以直接消耗掉这个凭证,所以可以先唤醒。
为什么LockSupprot唤醒两次后,阻塞两次,最终结果还是会阻塞线程?
因为凭证最多为1,不可累加。唤醒两次permit都只是1,两次park需要两次凭证。