了解过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就会阻塞线程。

推荐阅读更多精彩内容