写点什么

线程的三种等待唤醒机制(面试必问)

用户头像
hepingfly
关注
发布于: 2021 年 01 月 24 日
线程的三种等待唤醒机制(面试必问)

线程等待唤醒机制


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


方式一:使用 Object 中的 wait() 方法让线程等待,使用 Object 中的 notify() 方法唤醒线程


  • 方式二:使用 JUC 包中 Condition 的 await() 方法让线程等待,使用 signal() 方法唤醒线程


  • 方式三:LockSupport 类可以阻塞当前线程以及唤醒指定被阻塞的线程。


  • 1、Object 类中的 wait 和 notify 实现线程等待和唤醒

    1)、正常情况下代码实现

    /** * @auther hepingfly * @date 2021/1/18 10:39 下午 */public class LockSupportDemo {    static Object obj = new Object();  // 使用同一把锁    public static void main(String[] args) {        new Thread(() -> {            synchronized (obj) {                System.out.println(Thread.currentThread().getName() + "进入");                try {                    obj.wait();                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println(Thread.currentThread().getName() + "被唤醒");            }        },"线程1").start();        new Thread(() -> {            synchronized (obj) {                obj.notify();                System.out.println(Thread.currentThread().getName() + "通知");            }        },"线程2").start();    }  // ---------------上面是正常情况,打印出来的结果为:---------------------------------------线程1进入线程2通知线程1被唤醒
    复制代码


    2)、异常情况一(去掉 synchronized 关键字)

    /** * @auther hepingfly * @date 2021/1/18 10:39 下午 */public class LockSupportDemo {    static Object obj = new Object();    public static void main(String[] args) {        new Thread(() -> {                System.out.println(Thread.currentThread().getName() + "进入");                try {                    obj.wait();                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println(Thread.currentThread().getName() + "被唤醒");        },"线程1").start();        new Thread(() -> {                obj.notify();                System.out.println(Thread.currentThread().getName() + "通知");        },"线程2").start();    }}
    复制代码


    运行直接报异常:


    结论:

    Object 类中的 wait、notify、notifyAll 用于线程等待和唤醒的方法,都必须在 synchronized 内部执行。


    3)、异常情况二(将 notify 方法放在 wait 方法前面)

    /** * @auther hepingfly * @date 2021/1/18 10:39 下午 */public class LockSupportDemo {    static Object obj = new Object();    public static void main(String[] args) {        new Thread(() -> {            try {                Thread.sleep(3000); // 这里让线程1先睡上 3 秒,这样线程2就会先执行            } catch (InterruptedException e) {                e.printStackTrace();            }            synchronized (obj) {                System.out.println(Thread.currentThread().getName() + "进入");                try {                    obj.wait();                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println(Thread.currentThread().getName() + "被唤醒");            }        },"线程1").start();        new Thread(() -> {            synchronized (obj) {                obj.notify();                System.out.println(Thread.currentThread().getName() + "通知");            }        },"线程2").start();    }}
    复制代码


    执行结果为程序无法结束一直阻塞


    结论:

    必须要先 wait 后 notify 或者 notifyAll ,等待中的线程才会被唤醒,否则无法唤醒。


    2、Condition 接口中的 await 和 signal 方法实现线程等待和唤醒

    1)、正常情况下代码实现


    /** * @auther hepingfly * @date 2021/1/18 10:39 下午 */public class LockSupportDemo {    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() + "进入");               condition.await();           } catch (InterruptedException e) {               e.printStackTrace();           } finally {               lock.unlock();           }           System.out.println(Thread.currentThread().getName() + "被唤醒");        },"线程1").start();        new Thread(() -> {            lock.lock();            try {                condition.signal();                System.out.println(Thread.currentThread().getName() + "通知");            }  finally {                lock.unlock();            }        },"线程2").start();    }    // ---------------上面是正常情况,打印出来的结果为:---------------------------------------线程1进入线程2通知线程1被唤醒
    复制代码


    2)、异常情况一(去掉 lock 关键字)

    /** * @auther hepingfly * @date 2021/1/18 10:39 下午 */public class LockSupportDemo {    static Object obj = new Object();    static Lock lock = new ReentrantLock();    static Condition condition = lock.newCondition();    public static void main(String[] args) {        new Thread(() -> {           try {               System.out.println(Thread.currentThread().getName() + "进入");               condition.await();           } catch (InterruptedException e) {               e.printStackTrace();           } finally {           }           System.out.println(Thread.currentThread().getName() + "被唤醒");        },"线程1").start();        new Thread(() -> {            try {                condition.signal();                System.out.println(Thread.currentThread().getName() + "通知");            }  finally {            }        },"线程2").start();    } // 去掉 lock 和 unlock 关键字,仅仅保留 await() 和 signal() 方法,发现会直接报错
    复制代码


    运行直接报异常:


    结论:

    Condition 接口中的 await、signal、signalAll 用于线程等待和唤醒的方法,都必须在 lock 块内部执行


    3)、异常情况二(将 signal 方法放在 await 方法前面)

    /** * @auther hepingfly * @date 2021/1/18 10:39 下午 */public class LockSupportDemo {    static Object obj = new Object();    static Lock lock = new ReentrantLock();    static Condition condition = lock.newCondition();    public static void main(String[] args) {        new Thread(() -> {            try {                Thread.sleep(3000);    // 这里让线程1睡上 3 秒钟            } catch (InterruptedException e) {                e.printStackTrace();            }           lock.lock();           try {               System.out.println(Thread.currentThread().getName() + "进入");               condition.await();           } catch (InterruptedException e) {               e.printStackTrace();           } finally {               lock.unlock();           }           System.out.println(Thread.currentThread().getName() + "被唤醒");        },"线程1").start();        new Thread(() -> {            lock.lock();            try {                condition.signal();                System.out.println(Thread.currentThread().getName() + "通知");            }  finally {                lock.unlock();            }        },"线程2").start();    }
    复制代码


    执行结果为程序无法结束一直阻塞

    结论:

    必须要先 await 后 signal 或者 signalAll ,等待中的线程才会被唤醒,否则无法唤醒。


    3、LockSupport 类中的 park 等待和 unpark 唤醒

    我们主要是通过 park 和 unpark 方法来实现阻塞和唤醒线程的操作


    LockSupport 类使用了一种名为 Permit (许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit 只有两个值 1 和 0,默认是 0

    • 阻塞

    • park()/park(Object blocker)

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

    • 阻塞当前线程/阻塞传入的具体线程

    • 唤醒

    • unpark(Thread thread)

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

    • 唤醒处于阻塞状态的指定线程


    案例 1:先 park()unpark()

    /** * @auther hepingfly * @date 2021/1/18 10:39 下午 */public class LockSupportDemo {    /**     * 这个好处就是我可以单纯的去阻塞唤醒线程     * 而不需要去加一些锁块之类的代码     * @param args     */    public static void main(String[] args) {        Thread thread1 = new Thread(() -> {            System.out.println(Thread.currentThread().getName() + "进入");            LockSupport.park();  // 类似于 wait() 方法,被阻塞,等待通知,要放行的话需要许可证            System.out.println(Thread.currentThread().getName() + "被唤醒");        }, "线程1");        thread1.start();        // 暂停几秒钟        try {            Thread.sleep(3000);        } catch (InterruptedException e) {            e.printStackTrace();        }
    /** * 3秒钟以后,线程2运行,线程2要去唤醒线程1,相当于要给线程1发放许可证,线程1才能被放行 */ new Thread(() -> { System.out.println(Thread.currentThread().getName() + "通知"); LockSupport.unpark(thread1); },"线程2").start(); }}
    // -----------------------------运行结果线程1进入线程2通知线程1被唤醒
    复制代码


    案例 2:先 unpark()park()


    public class LockSupportDemo { // 先 unpark 后 park 也是可以运行成功的,不像 wait 和 notify 会直接报错 public static void main(String[] args) { Thread thread1 = new Thread(() -> { // 暂停几秒钟 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "进入"); LockSupport.park(); // 如果先 unpark 后 park , 那么这行会直接不起作用 System.out.println(Thread.currentThread().getName() + "被唤醒"); }, "线程1"); thread1.start();
    new Thread(() -> { System.out.println(Thread.currentThread().getName() + "通知"); LockSupport.unpark(thread1); },"线程2").start(); }}//-------------------运行结果线程2通知线程1进入线程1被唤醒
    复制代码


    总结:


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


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


    LockSupport 提供 park()unpark() 方法实现阻塞线程和解除线程阻塞的过程


    LockSupport 和每个使用它的线程都有一个许可(permit)关联。 permit 默认是 0。

    • 调用一次 unpark 就加 1 变成 1

    • 调用一次 park 会消费 permit ,也就是将 1 变成 0,同时 park 立即返回。

    • 如果再次调用 park 会变成阻塞(因为 permit 为 0 会阻塞在这里,一直到 permit 为 1),这时候调用 unpark 会把 permit 置为 1

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


    简单来说:


    线程阻塞需要消耗凭证(permit),这个凭证最多只有一个。

    • 当调用 park 方法时

    • 如果有凭证,则会直接消耗掉这个凭证然后正常退出

    • 如果没有凭证,就必须阻塞等待凭证可用

    • 当调用 unpark 方法时

    • 它会增加一个凭证,但凭证最多只能有一个,无法累加。


    面试题:


    为什么可以先唤醒线程后阻塞线程?

    答:先唤醒线程意味着你调用了 unpark() 方法,那么凭证加 1,再去阻塞线程,即调用 park() 方法,这个时候有凭证,所以直接消耗掉凭证然后正常退出


    为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

    答:唤醒两次意味着调用了两次 unpark() 方法,但是凭证无法累加最多只有 1,然后阻塞两次,即调用两次 park() 方法,需要消费 2 张凭证才能正常退出,但是只有 1 张凭证,所以凭证不够,阻塞。


    更多有意思的内容可以关注我的视频号


    每天更新一个视频,给你分享一个干货


    发布于: 2021 年 01 月 24 日阅读数: 29
    用户头像

    hepingfly

    关注

    视频号:hepingfly 分享干货,欢迎关注~ 2018.06.23 加入

    B站程序员。目标是做一个有才华,身上有光的男人。

    评论

    发布
    暂无评论
    线程的三种等待唤醒机制(面试必问)