JUC(6)LockSupport

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

线程等待唤醒机制

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

  • 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
  • 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
  • LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

wait()和notify()存在的不足

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

        //暂停几秒钟线程
        try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}

        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();
            }
        }, "t2").start();
    }
}

这段代码运行完全没有任何问题,一个很正常的等待-唤醒流程。

但是,wait()和notify()方法能否脱离同步代码块呢?当我们作出这个尝试运行后发现,代码报了java.lang.IllegalMonitorStateException异常

那么我们能否更换wait()和notify()的执行顺序呢?也就是先执行notify方法在执行wait方法,我们发现,代码虽然表面上没有报错,但是执行wait的那个线程一直在等待下一个唤醒他的线程。

故而我们得出结论· wait和notify方法必须要在同步块或者方法里面,且成对出现使用,先wait后notify才OK。

await()和signal()存在的不足

public class LockSupportDemo {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t" + "start");
                condition.await();
                System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t1").start();
      
        //暂停几秒钟线程
        try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}

        new Thread(() -> {
            lock.lock();
            try {
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "通知了");
        }, "t2").start();
    }
}

与之前的尝试一样,await和signal方法能否脱离lock()方法使用呢?显然不行,和同步代码块一样,也报了java.lang.IllegalMonitorStateException异常,同样的先调用signal方法再调用await方法也会导致调用await的方法一直阻塞。

所以对于await和signal方法,我们可以得出结论

  • Condtion中的线程等待和唤醒方法之前,需要先获取锁
  • 一定要先await后signal,不要反了

以上两组方法的测试我们可以得出结论

  • 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
  • 必须要先等待后唤醒,线程才能够被唤醒

LockSupport

· 通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作

This class associates, with each thread that uses it, a permit (in the sense of the Semaphore class). A call to park will return immediately if the permit is available, consuming it in the process; otherwise it may block. A call to unpark makes the permit available, if it was not already available. (Unlike with Semaphores though, permits do not accumulate. There is at most one.)

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

pack()-阻塞当前线程/阻塞传入的具体线程

    public static void park() {
        UNSAFE.park(false, 0L);
    }

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

unpack()-唤醒处于阻塞状态的指定线程

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

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

public class LockSupportDemo {
    public static void main(String[] args) {
        //正常使用+不需要锁块
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " " + "1111111111111");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + " " + "2222222222222------end被唤醒");
        }, "t1");
        t1.start();
        
        //暂停几秒钟线程
        try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}
      
        LockSupport.unpark(t1);
        System.out.println(Thread.currentThread().getName() + "   -----LockSupport.unparrk() invoked over");

    }
}

上述代码明显它不需要必须在锁块中调用,那么如果我们先执行unpack方法再执行pack方法,pack所在的线程会被阻塞吗?不会!因为unpack方法执行后,permit被置为1,当pack方法需要时,拿到permit发现是1,消费即可,不会阻塞

他解决了之前线程等待唤醒机制存在的两大问题

  1. LockSupport不用持有锁块,不用加锁,程序性能好

  2. 先后执行顺序不影响是否导致阻塞

值得注意的是,如果我们先执行两次unpack方法,再执行两次pack方法,我们发现,线程被阻塞住了,因为unpack两次,并不会+2,许可证仍是1,第一个pack方法使用后置为0,第二个pack没有许可证也就阻塞住了。

总结

  • LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。本质调用的Unsafe中的native方法。

  • LockSupport和每个使用它的线程都有一个许可(permit)关联,permit相当于0和1的开关,默认是0.

  • 每个线程都有一个相关的permit,permit最多有一个,重复调用unpack不会积累凭证。

  • 当调用pack方法时

    • 如果有凭证,消耗凭证正常结束阻塞
    • 如果没有凭证,就阻塞直到凭证可用(为1)
  • 当调用unpack方法时,它会增加一个凭证,但凭证最多只能有一个,累加无效

  • 调用被pack方法阻塞的线程的interrupt方法,也会唤醒该线程

posted @ 2021-08-05 23:30  Zoran0104  阅读(53)  评论(0编辑  收藏  举报