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却需要消费两个凭证,证不够,不能放行。

posted @ 2021-05-23 23:37  Java小白的搬砖路  阅读(243)  评论(0编辑  收藏  举报