zoukankan      html  css  js  c++  java
  • JUC下线程的三种等待唤醒机制

    第一种:Object类中的wait和notify方法实现线程的等待和唤醒

    下面标黄字的部分就是对一下两点总结的实现:

    • 不能脱离synchronized代码块使用,否则会抛出IllegalMonitorStateException异常
    • 先wait后notify、notifyAll,等待中的线程才能被唤醒,顺序不能改变

    演示代码:

    /**
     * @author zhangzhixi
     * @date 2021-5-23 18:21
     */
    public class LockDemo {
        static Object object = new Object();
    
        public static void main(String[] args) {
            new Thread(() -> {
                synchronized (object) {
                    System.out.println(Thread.currentThread().getName() + "==>线程Come in");
                    try {
                        // 使当前线程等待
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "==>线程被唤醒");
                }
            }, "A").start();
    
            new Thread(() -> {
                synchronized (object) {
                    System.out.println(Thread.currentThread().getName() + "==>通知线程");
                    // 使当前线程唤醒
                    object.notifyAll();
                }
            }, "B").start();
        }
    }
    

    我们来看下上面代码的执行结果,看起来很和谐:

     我们来将上面代码动下手脚,注释掉10、23代码看下wait/notify脱离了Synchronized会出现什么?

    可以看到报了异常,说明wait/notify不能够脱离Synchronized代码块


     我们再来看一下使线程A暂停三秒会发生什么?

    线程B先执行了,没有代码将线程A的wait状态进行唤醒

    第二种:Condition接口中的await和single方法实现线程的等待和唤醒

    • 必须配合lock()方法使用,否则抛出IllegalMonitorStateException异常
    • 等待唤醒调用顺序不能改变

     代码实现:

    public class LockDemo {
        static Object object = new Object();
        // 创建可重入锁
        static Lock lock = new ReentrantLock();
        static Condition condition = lock.newCondition();
    
        public static void main(String[] args) {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "==>线程Come in");
                    // 使当前线程等待
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
                System.out.println(Thread.currentThread().getName() + "==>线程被唤醒");
            }, "A").start();
    
            new Thread(() -> {
    
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "==>通知线程");
                    // 使当前线程唤醒
                    condition.signal();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }, "B").start();
        }
    }
    

    看下上面代码的执行结果:

      

     我们来将上面代码动下手脚,注释掉9 17 24 32行代码看下wait/notify脱离了Synchronized会出现什么?

    可以看到报了异常,说明wait/notify不能够脱离Synchronized代码块

    我们再来看一下使线程A暂停三秒会发生什么?

    线程B先执行了,没有代码将线程A的await状态进行唤醒

     

    第三种:LockSupport类中的park等待和unpark唤醒

    LockSupport提供park()和unpark()方法实现阻塞和唤醒线程的过程
    LockSupport和每一个使用它的线程之间有一个许可(permit)进行关联,permit有0和1两种状态,默认为0,即无许可证状态。
    调用一次unpark方法,permit加1变成1。每次调用park方法都会检查许可证状态,如果为1,则消耗掉permit(1 -> 0)并立刻返回;如果为0,则进入阻塞状态。permit最多只有一个,重复调用unpark也不会累积permit。

    /**
     * @author zhangzhixi
     * @date 2021-5-23 18:21
     */
    public class LockDemo {
        static Object object = new Object();
        // 创建可重入锁
        static Lock lock = new ReentrantLock();
        static Condition condition = lock.newCondition();
    
        public static void main(String[] args) {
            Thread a = new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "==>Come in");
                LockSupport.park();//阻塞当前线程
                System.out.println(Thread.currentThread().getName() + "==>被唤醒");
    
            });
            a.setName("a");
            a.start();
    
            new Thread(() -> {
                LockSupport.unpark(a);
                System.out.println(Thread.currentThread().getName() + "==>通知");
            }, "B").start();
        }
    }
    

    看下上面代码的执行结果:


    下面给线程A进行等待3S,看会不会像一,二两个例子出现错误情况:

    得出结论是:

      因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

     总结LockSupport:

    LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
    LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根
    结底,LockSupport调用的Unsafe中的native代码。
     
    LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
    LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,
    调用一次unpark就加1变成1,
    调用一次park会消费permit,也就是将1变成o,同时park立即返回。
    如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。
    每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。
     
    形象的理解
    线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
    当调用park方法时
        *如果有凭证,则会直接消耗掉这个凭证然后正常退出;
        *如果无凭证,就必须阻塞等待凭证可用;
    而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
     
     面试题

    为什么可以先唤醒线程后阻塞线程?
    因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。
     
     
    为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
    因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;
    而调用两次park却需要消费两个凭证,证不够,不能放行。

  • 相关阅读:
    Serverless 工程实践 | Serverless 应用开发观念的转变
    如何高效学习 Kubernetes 知识图谱?
    互动赠新书|当云原生遇到混合云:如何实现“求变”与“求稳”的平衡
    5 款阿里常用代码检测工具,免费用!
    AI与传统编译器
    OpenArkCompiler方舟编译
    传统编译原理
    LLVM基础技术图例
    双极型与低频大功率晶体管
    TVM,Relay,Pass
  • 原文地址:https://www.cnblogs.com/zhangzhixi/p/14802614.html
Copyright © 2011-2022 走看看