了解过LockSupport吗
1. 是什么?
LockSupport是JUC包下的一个类,是用来创建锁和其他同步类的基本线程阻塞原语。
相信大多数人看了这句话也还是不太明白它到底是啥东西,那你还记得等待唤醒机制吗?之前实现等待唤醒机制可以用wait/notify,可以用await/signal,这个LockSupport就是它们的改良版。
2. 等待唤醒机制:
先来回顾一下等待唤醒机制。
先看看用wait/notify实现:
- wait/notify:
private static Object lockObj = new Object();
private static void waitNotify() {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockObj){
System.out.println("线程" + Thread.currentThread().getName() + "进来了");
try { lockObj.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("线程" + Thread.currentThread().getName() + "被唤醒");
}
}, "A").start();
new Thread(() -> {
synchronized (lockObj){
System.out.println("线程" + Thread.currentThread().getName() + "进来了");
lockObj.notify();
System.out.println("线程" + Thread.currentThread().getName() + "唤醒另一个线程");
}
}, "B").start();
}
这段代码就很简单了,线程A先wait,线程B去notify,线程B执行完了A就被唤醒了,这就是最开始学的等待唤醒机制。
假如我现在注释掉synchronized,如下:
new Thread(() -> {
//synchronized (lockObj){
System.out.println("线程" + Thread.currentThread().getName() + "进来了");
try { lockObj.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("线程" + Thread.currentThread().getName() + "被唤醒");
//}
}, "A").start();
再次运行,结果报错了,如下:

得出第一个结论:wait/notify必须中同步代码块中才能使用。
如果先执行notify,再执行wait,情况如何呢?让A线程睡3秒钟,使B先执行,先notify,然后A中wait,执行结果如下:

可以发现”线程A被唤醒“这句话一直没有打印出来。
得出第二个结论:先notify再wait的话,程序无法被唤醒。
再来看看用await/notify实现:
- await/signal:
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private static void awaitSignal(){
new Thread(() -> {
lock.lock();
try {
System.out.println("线程" + Thread.currentThread().getName() + "进来了");
condition.await();
System.out.println("线程" + Thread.currentThread().getName() + "被唤醒");
} catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}, "A").start();
new Thread(() -> {
try {
lock.lock();
System.out.println("线程" + Thread.currentThread().getName() + "进来了");
condition.signal();
System.out.println("线程" + Thread.currentThread().getName() + "唤醒另一个线程");
} catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}, "B").start();
}
这个是用await/signal实现的等待唤醒机制。
假如注释掉lock和unlock这两个操作,再次执行,还是会抛之前那个异常,即IllegalMonitorStateException。
得出第一个结论:await/notify必须伴随lock/unlock出现。
假如先signal,再await,情况也是和之前用wait/notify一样,await的线程一直没被唤醒。
得出第二个结论:必须先await再signal。
所以不管是wait/notify还是await/signal,都有两个限制条件:
-
线程要先获得并持有锁,必须中锁块中执行;
-
必须先等待后唤醒。
3. LockSupport怎么用?
LockSupport主要就是用park(等待)和unpark(唤醒)方法来实现等待唤醒。它的原理就是使用了一种名为permit(许可证)的概念来实现等待唤醒功能,每个线程都有一个许可证,许可证只有两个值,一个是0,一个是1。默认值是0,表示没有许可证,就会被阻塞。那谁来发放许可证呢,就是unpark方法。这两个方法底层其实是UNSAFE类的park和unpark方法,调用park时,将permit的值设置为0,调用unpark时,将permit的值设置为1。
用法如下:
private static void lockSupportTest(){
Thread a = new Thread(() -> {
System.out.println("线程" + Thread.currentThread().getName() + "进来了");
LockSupport.park(); // 等待
System.out.println("线程" + Thread.currentThread().getName() + "被唤醒");
}, "A");
a.start();
Thread b = new Thread(() -> {
System.out.println("线程" + Thread.currentThread().getName() + "进来了");
LockSupport.unpark(a); // 唤醒
}, "B");
b.start();
}
首先它不需要再同步块中使用,这是第一个优点。那么先unpark再park会不会报错呢?要知道另两种方式先唤醒再等待的话,都会导致线程无法被唤醒的。假如我先unpark,再park,其实也是可以的,相当于提前发放了通行证,先给A线程unpark了,那么A线程执行的时候,就相当于没有park这一行。
LockSupport总结:是一个线程阻塞唤醒的工具类,所有方法都是静态方法,可以让线程在任意位置阻塞,其底层调用的是UNSAFE类的native方法。每调用一次unpark方法,permit就会变成1,每调一次park方法,就会消耗掉一个许可证,即permit就变成0,每个线程都有一个permit,permit最多也就一个,多次调用unpark也不会累加。因为这是根据是否有permit去判断是否要阻塞线程的,所以,先unpark再park也可以,跟顺序无关,只看是否有permit。如果先unpark了两次,再park两次,那么线程还是会被阻塞,因为permit不会累加,unpark两次,permit的值还是1,第一次park的时变成0了,所以第二次park就会阻塞线程。