JUC下线程的三种等待唤醒机制

 


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

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

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

演示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * @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异常
  • 等待唤醒调用顺序不能改变

 代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * @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 @   Java小白的搬砖路  阅读(248)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话

喜欢请打赏

扫描二维码打赏

支付宝打赏

点击右上角即可分享
微信分享提示