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


六、总结

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

相关文章
|
1月前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
60 9
|
1月前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
38 3
|
2月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
24 1
|
2月前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
32 1
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
51 1
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
58 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
27 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
24 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
38 2
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
45 1