JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)

简介: JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)

JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)


文章目录

一、前言

二、wait/notify的使用

1. 代码演示

2. 执行结果

3. 测试不在代码块执行(`把上面代码注释1给删除`)

4. 修改代码

5. 总结

三、await/signal的使用

1. 代码演示

2. 执行结果

3. 测试不在代码块执行(`把上面代码注释1给删除`)

4. 修改代码

5. 总结

四、LockSupport的park/unpark的使用

1. LockSupport介绍

2. park源码查看

3. unpark源码查看

4. 代码演示

5. 结果展示

6. 修改代码

7. 与前两者比的优点

8.总结

9.白话文理解

五、面试题

六、总结


一、前言

在多线程的场景下,我们会经常使用加锁,来保证线程安全。如果锁用的不好,就会陷入死锁,我们以前可以使用Object的wait/notify来解决死锁问题。也可以使用Condition的await/signal来解决,当然最优还是LockSupport的park/unpark。他们都是解决线程等待和唤醒的。下面来说说具体的优缺点和例子证明一下。


二、wait/notify的使用

1. 代码演示

public class JUC {
    static Object lock = new Object();
    public static void main(String[] args) {
        new Thread(()->{
            synchronized (lock) {// 1
                System.out.println(Thread.currentThread().getName() + "进来");
                try {
                    // 释放锁,陷入阻塞,直到有人唤醒
                    lock.wait();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }// 1
            System.out.println(Thread.currentThread().getName() + "我被唤醒了");
        }, "A").start();
        new Thread(()->{
            synchronized (lock) {// 2
                lock.notify();
                System.out.println(Thread.currentThread().getName() + "随机唤醒一个线程");
            }// 2
        }, "B").start();
    }
}

2. 执行结果

3. 测试不在代码块执行(把上面代码注释1给删除

4. 修改代码

try {
    TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
    e.printStackTrace();
}

5. 总结

wait和notify方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException。


调用顺序要先wait后notify才可以正常阻塞和唤醒。


三、await/signal的使用

1. 代码演示

public class JUC {
    static ReentrantLock reentrantLock = new ReentrantLock();
    static Condition condition = reentrantLock.newCondition();
    public static void main(String[] args) {
        new Thread(()->{
            reentrantLock.lock();// 1
            try {
                System.out.println(Thread.currentThread().getName()+"进来");
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();// 1
            }
            System.out.println(Thread.currentThread().getName()+"我被唤醒了");
        },"A").start();
        new Thread(()->{
            reentrantLock.lock();// 1
            try {
                condition.signal();
                System.out.println(Thread.currentThread().getName()+"随机唤醒一个线程");
            }finally {
                reentrantLock.unlock();// 1
            }
        },"B").start();
    }
}

2. 执行结果

3. 测试不在代码块执行(把上面代码注释1给删除

4. 修改代码

try {
    TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
    e.printStackTrace();
}

5. 总结

await和signal方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException。


调用顺序要先await后signal才可以正常阻塞和唤醒。——和wait/notify一致。


四、LockSupport的park/unpark的使用

1. LockSupport介绍

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


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


可以把许可看成是一种(0、1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。

2. park源码查看

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}
public static void park() {
    UNSAFE.park(false, 0L);
}

作用:park()/park(Object blocker) - 阻塞当前线程阻塞传入的具体线程

我们会发现底层是调用sun.misc.Unsafe:这个类的提供了一些绕开JVM的更底层功能,基于它的实现可以提高效率。

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

3. unpark源码查看

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

作用:unpark(Thread thread) - 唤醒处于阻塞状态的指定线程

我们会发现底层都是调用sun.misc.Unsafe。

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

4. 代码演示

public class JUC {
    public static void main(String[] args) {
        Thread a = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "进来");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + " 被换醒了");
        }, "A");
        a.start();
        Thread b = new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.unpark(a);
            System.out.println(Thread.currentThread().getName()+"唤醒传入的线程");
        }, "B");
        b.start();
    }
}

5. 结果展示

6. 修改代码

try {
  TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
  e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "进来" + System.currentTimeMillis());
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " 被换醒了" + System.currentTimeMillis());

7. 与前两者比的优点

park/unpark不需要在同步块或者方法内才能执行,解决了上面两种不在同步块或者方法就报错的情况。


park/unpark不需要先执行park,在执行unpark,无需在意顺序。解决了上面两种必须有前后顺序的情况。

8.总结

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


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

public native void park(boolean var1, long var2);

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


LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成0,同时park立即返回。


如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证。

阻塞原因:根据上面代码,我们会先执行线程B,调用unpark方法,虽然进行两次unpark。但是只有一个有效,此时permit为1。此时A线程开始,来到第一个park,permit消耗后为0,为0是阻塞,等待unpark,此时没有unpark了,所以一直陷入阻塞。

9.白话文理解

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

当调用park方法时

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

如果无凭证,就必须阻塞等待凭证可用。

而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无放。


五、面试题

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


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


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


因为凭证的数量最多为1(不能累加),连续调用两次 unpark和调用一次 unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。


六、总结

看到这里的小伙伴,点个赞不过分吧,小编也是整理了一下午,参考阳哥课件。

目录
打赏
0
0
1
0
88
分享
相关文章
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
面试必看:如何设计一个可以优雅停止的线程?
嘿,大家好!我是小米。今天分享一篇关于“如何停止一个正在运行的线程”的面试干货。通过一次Java面试经历,我明白了停止线程不仅仅是技术问题,更是设计问题。Thread.stop()已被弃用,推荐使用Thread.interrupt()、标志位或ExecutorService来优雅地停止线程,避免资源泄漏和数据不一致。希望这篇文章能帮助你更好地理解Java多线程机制,面试顺利! 我是小米,喜欢分享技术的29岁程序员。欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
103 53
面试大神教你:如何巧妙回答线程优先级这个经典考题?
大家好,我是小米。本文通过故事讲解Java面试中常见的线程优先级问题。小明和小华的故事帮助理解线程优先级:高优先级线程更可能被调度执行,但并非越高越好。实际开发需权衡业务需求,合理设置优先级。掌握线程优先级不仅能写出高效代码,还能在面试中脱颖而出。最后,小张因深入分析成功拿下Offer。希望这篇文章能助你在面试中游刃有余!
42 4
面试大神教你:如何巧妙回答线程优先级这个经典考题?
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
86 14
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
52 13
面试中的难题:线程异步执行后如何共享数据?
本文通过一个面试故事,详细讲解了Java中线程内部开启异步操作后如何安全地共享数据。介绍了异步操作的基本概念及常见实现方式(如CompletableFuture、ExecutorService),并重点探讨了volatile关键字、CountDownLatch和CompletableFuture等工具在线程间数据共享中的应用,帮助读者理解线程安全和内存可见性问题。通过这些方法,可以有效解决多线程环境下的数据共享挑战,提升编程效率和代码健壮性。
60 6
面试官最爱的面试题:wait() 和 notify() 为什么需要同步?
大家好,我是小米。今天来探讨一个常见的Java面试题:为什么线程通信的 `wait()`、`notify()` 和 `notifyAll()` 方法被定义在 Object 类里,且必须在同步方法或同步块中调用?通过小明和小红的工作场景,我们理解了这些方法的核心思想——线程间的协调与通信。它们依赖于对象锁,确保线程按预期顺序执行,避免资源争抢和死锁。掌握这些知识点,能帮助你更好地应对多线程相关的面试问题。如果你对线程同步等话题感兴趣,欢迎继续交流。
47 12
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
82 11
面试必问的多线程优化技巧与实战
多线程编程是现代软件开发中不可或缺的一部分,特别是在处理高并发场景和优化程序性能时。作为Java开发者,掌握多线程优化技巧不仅能够提升程序的执行效率,还能在面试中脱颖而出。本文将从多线程基础、线程与进程的区别、多线程的优势出发,深入探讨如何避免死锁与竞态条件、线程间的通信机制、线程池的使用优势、线程优化算法与数据结构的选择,以及硬件加速技术。通过多个Java示例,我们将揭示这些技术的底层原理与实现方法。
139 3
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等