• 【JUC】Java并发编程从挖坑到入土全解(4-一文讲通LockSupport与线程中断->长图预警)


    目录

    LockSupport与线程中断

    线程中断机制

    什么是中断机制?

    与中断相关的3个API

    如何停止中断运行中的线程?

    当前线程的中断标识为true,是不是线程就会立刻停止?

    如何理解静态方法Thread.interrupted()

    LockSupport是什么

    线程等待和唤醒机制

    3种让线程等待唤醒的方法

    Object类中的wait()和notify()方法实现线程的等待和唤醒

    Condition接口中的await()后signal()方法实现线程的等待和唤醒

    上述两个对象Object和Condition使用的限制条件

    LockSupport类中的park()等待和unpack()唤醒

    总结


    LockSupport与线程中断

    线程中断机制

    什么是中断机制?

    首先,一个线程不应该由其他线程强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运(所以,Thread的stop()、suspend()、resume()都已经废弃了)

    其次,在Java中没有办法立即停止一条线程,然而停止线程又显得那么重要(比如需要取消一个耗时/错误操作)。因此,Java提供了一种用于停止线程的协商机制——中断,也即中断标识协商机制

    中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完成完全要求程序员自己实现。若要中断一个线程,需要手动调用该线程的interrupt()方法,该方法也仅仅是将线程对象的中断标识设成true,接着按自己的需要,写代码不断监测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟该做什么需要自己写代码实现。

    每个线程对象中都有一个中断标识位,用于表示线程是否被中断:该标识位为true标识中断,为false表示未中断。通过调用线程对象的interrupt方法将该线程的标识位设为true,可以在别的线程中带调用,也可以在自己的线程中调用。

    与中断相关的3个API
    • public void interrupt():设置线程的中断状态为true,发起一个协商而不会立刻停止线程
    • public static boolean isInterrupted():通过检查中断标识位判断线程是否被中断并清除当前中断状态(返回当前线程的中断状态+重置中断状态为false)
    • public boolean isInterrupted():通过检查中断标识位判断当前线程是否被中断
    如何停止中断运行中的线程?
    • 通过volatile变量实现(线程间的可见性)
    • 通过AtomicBoolean实现

    执行结果如下图:

    • 通过Thread类自带的中断API实例方法实现 在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程
      • interrupt() 中断线程

    我们可以看到interrupt0()是一个native方法

    除非线程正在中断(始终允许),否则将带调用此线程的checkAccess()方法(可能还会导致抛出SecurityException)

    1. public class InterruptDemo {
    2. static volatile boolean isStop = false;
    3. public static void main(String[] args) {
    4. Thread tA = new Thread(() -> {
    5. while (true) {
    6. if (isStop) {
    7. System.out.printf(Thread.currentThread().getName() + " isStop 被修改为true!");
    8. break;
    9. }
    10. System.out.println("-------------------->");
    11. }
    12. }, "tA");
    13. tA.start();
    14. try {
    15. TimeUnit.MILLISECONDS.sleep(20);
    16. } catch (InterruptedException e) {
    17. e.printStackTrace();
    18. }
    19. new Thread(() -> {
    20. isStop = true;
    21. }, "B").start();
    22. }
    23. }

    执行结果如下图:

    1. public class InterruptDemo {
    2. static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
    3. public static void main(String[] args) {
    4. Thread tA = new Thread(() -> {
    5. while (true) {
    6. if (atomicBoolean.get()) {
    7. System.out.printf(Thread.currentThread().getName() + " atomicBoolean 被修改为true!");
    8. break;
    9. }
    10. System.out.println("-------------------->");
    11. }
    12. }, "tA");
    13. tA.start();
    14. try {
    15. TimeUnit.MILLISECONDS.sleep(20);
    16. } catch (InterruptedException e) {
    17. e.printStackTrace();
    18. }
    19. new Thread(() -> {
    20. atomicBoolean.set(true);
    21. }, "B").start();
    22. }
    23. }
    1. package com.aqin.juc;
    2. import java.util.concurrent.TimeUnit;
    3. import java.util.concurrent.atomic.AtomicBoolean;
    4. /**
    5. * @Description
    6. * @Author aqin1012 AQin.
    7. * @Date 10/9/23 11:29 AM
    8. * @Version 1.0
    9. */
    10. public class InterruptDemo {
    11. public static void main(String[] args) {
    12. Thread tA = new Thread(() -> {
    13. while (true) {
    14. if (Thread.currentThread().isInterrupted()) {
    15. System.out.printf(Thread.currentThread().getName() + " isInterrupted() 被修改为true!程序停止!");
    16. break;
    17. }
    18. System.out.println("-------------------->");
    19. }
    20. }, "tA");
    21. tA.start();
    22. try {
    23. TimeUnit.MILLISECONDS.sleep(20);
    24. } catch (InterruptedException e) {
    25. e.printStackTrace();
    26. }
    27. new Thread(() -> {
    28. tA.interrupt();
    29. }, "B").start();
    30. }
    31. }

    执行效果如下图:

    当前线程的中断标识为true,是不是线程就会立刻停止?

    不会,具体来说,对一个线程,调用interrupt()时,如果线程处于正常活动状态,那么会将该线程的中断标志设置为true(仅此而已),被设置中断标志的线程将继续正常运行,不受影响。所以,interrupt()并不是真正的中断线程,需要被调用的线程自己进行配合才行。如果线程处于被阻塞状态(如sleep、wait、join等状态),在别的线程中调用当前线程对象的interrupt()方法时,那么线程将会立即退出被阻塞状态,并抛出一个InterruptedException异常。

    如何理解静态方法Thread.interrupted()

    这个方法是判断线程是否中断并清除当前中断状态的,简单来说就是“复位”。

    主要做了两件事:

    1. 返回当前线程的中断状态,测试当前线程是否已经被中断
    2. 将当前线程的中断状态清零并重新设置为false,清除线程的中断状态

    那么问题来了,这个静态方法interrupted()跟实例方法isInterrupted()有什么区别呢?

    我们举个简单的例子对比下执行结果

    静态方法interrupted()

    执行结果如下:

    实例方法isInterrupted()

    执行结果如下

    来看它俩的源码对比

    可以看到其实它们调用的是同一个方法,只不过,在对于是否需要清理这个参数的传递上,静态方法传递的是true,而实例方法传递的是false,简单来讲就是多了一步“复位”操作。因此,才会出现上面两段示例代码执行的结果不一致,在当前线程未执行中断方法interrupt()时,当前线程的中断标识位本身就是初始值false,因此连续调用两次静态方法或者实例方法的执行结果都是false;当当前线程执行了中断方法interrupt()时,第一次调用静态方法和实例方法的返回值同样都是true,但是此时静态方法多做了一步“复位”操作,把当前线程的中断标识位重置回了初始值false,而实例方法则没有这步操作,因此,当第二次调用时,实例方法的示例中当前线程的中断标识位仍然是true,因此仍然返回true,而静态方法的示例代码中当前线程的中断标识位已经被重置回了false,于是就返回了false。

    LockSupport是什么

    LockSupport是java.util.concurrent.locks中的一个类,用于创建锁和其他同步类的基本线程阻塞原语

    线程等待和唤醒机制

    3种让线程等待唤醒的方法
    1. 使用Object中的wait()方法让线程等待,使用notify()方法唤醒线程
    2. 使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程
    3. 使用LockSupport类中的park()和unpark()方法阻塞当前线程以及唤醒指定被阻塞的线程
    Object类中的wait()和notify()方法实现线程的等待和唤醒

    示例代码:

    1. public static void main(String[] args) {
    2. Object objectLock = new Object();
    3. new Thread(() -> {
    4. try {
    5. TimeUnit.SECONDS.sleep(1);
    6. } catch (InterruptedException e) {
    7. e.printStackTrace();
    8. }
    9. synchronized (objectLock) {
    10. System.out.println(Thread.currentThread().getName() + " 进入--->");
    11. try {
    12. objectLock.wait();
    13. } catch (InterruptedException e) {
    14. e.printStackTrace();
    15. }
    16. System.out.println(Thread.currentThread().getName() + " 被唤醒^ ^");
    17. }
    18. }, "A").start();
    19. try {
    20. TimeUnit.SECONDS.sleep(1);
    21. } catch (InterruptedException e) {
    22. e.printStackTrace();
    23. }
    24. new Thread(() -> {
    25. synchronized (objectLock) {
    26. objectLock.notify();
    27. }
    28. System.out.println(Thread.currentThread().getName() + " 发出唤醒通知( ̄∇ ̄)/");
    29. }, "B").start();
    30. }

    执行结果如下:

    问题:

    • 必须要先持有锁,否则会报错

    • 必须先wait()再notify(),否则会卡死

    Condition接口中的await()后signal()方法实现线程的等待和唤醒

    示例代码:

    1. public static void main(String[] args) {
    2. Lock lock = new ReentrantLock();
    3. Condition condition = lock.newCondition();
    4. new Thread(() -> {
    5. try { TimeUnit.SECONDS.sleep(1); } catch ( InterruptedException e) { e.printStackTrace();}
    6. lock.lock();
    7. System.out.println(Thread.currentThread().getName() + " 进入--->");
    8. try {
    9. condition.await();
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. } finally {
    13. lock.unlock();
    14. }
    15. System.out.println(Thread.currentThread().getName() + " 被唤醒^ ^");
    16. }, "A").start();
    17. try { TimeUnit.SECONDS.sleep(1); } catch ( InterruptedException e) { e.printStackTrace();}
    18. new Thread(() -> {
    19. lock.lock();
    20. try {
    21. condition.signal();
    22. } finally {
    23. lock.unlock();
    24. }
    25. System.out.println(Thread.currentThread().getName() + " 发出唤醒通知( ̄∇ ̄)/");
    26. }, "B").start();
    27. }

    执行结果如下:

    问题:

    • 必须要先持有锁,否则会报错

    在lock、unlock对里面才能正确调用condition中线程等待和唤醒的方法。

    • 必须先await()再signal(),否则会卡死

    上述两个对象Object和Condition使用的限制条件
    • 线程先要获得并持有锁,必须在锁块(synchronized/lock)中
    • 必须要先等待后唤醒,线程才能够被唤醒
    LockSupport类中的park()等待和unpack()唤醒

    LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每一个线程都有一个Permit(许可),但与Semaphore不同的是,许可的累加上限是1(最多一个许可)。

    使用示例:

    1. public static void main(String[] args) {
    2. Thread A = new Thread(() -> {
    3. try {
    4. TimeUnit.SECONDS.sleep(1);
    5. } catch (InterruptedException e) {
    6. e.printStackTrace();
    7. }
    8. System.out.println(Thread.currentThread().getName() + " 进入--->");
    9. LockSupport.park();
    10. System.out.println(Thread.currentThread().getName() + " 被唤醒^ ^");
    11. }, "A");
    12. A.start();
    13. try {
    14. TimeUnit.SECONDS.sleep(1);
    15. } catch (InterruptedException e) {
    16. e.printStackTrace();
    17. }
    18. new Thread(() -> {
    19. LockSupport.unpark(A);
    20. System.out.println(Thread.currentThread().getName() + " 发出唤醒通知( ̄∇ ̄)/");
    21. }, "B").start();
    22. }

    执行结果如下:

    可以看到,使用LockSupport进行线程阻塞/唤醒不需要在锁对块(synchronized/lock)中,所以上面Object类和Condition接口的第一个问题解决了,然后我们看看第二个问题:加锁/解锁的先后顺序。

    如上图可以看出,park()和unpack()执行的先后顺序不会影响结果。

    总结

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

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

    LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程,每个使用LockSupport的线程都一个Permit(许可)与LockSupport相关联,每一个线程都都一个相关的Permit,并且最多一个,重复调用unpark()也不会积累Permit。

    换句话讲,线程阻塞需要消耗凭证,并且这个凭证最多1个

    当调用park()方法时:

    • 如果有Permit,会消耗这个Permit,然后正常退出
    • 如果没有Permit,会阻塞当前线程等待获取凭证

    当调用unpark()方法时:

    • 会增加一个Permit
    • 调用多次也只会有一个Permit(不累加)

    最后,我们思考2个问题:

    1. 为什么LockSupport提供park()和unpark()方法可以突破Object和Condition的调用先后顺序的限制? 因为调用unpark()会获得1个Permit,之后再调用park()会消耗这个Permit,park()和unpark()方法的执行顺序不会影响线程的唤醒,即便先发放了Permit仍然不会阻塞线程。
    2. 唤醒两次(调用两次park()方法)后阻塞两次(调用两次unpark)方法),会发生什么情况? 线程会被阻塞,因为Permit不会累加,即使调用了两次unpark(),仍然只会有一个Permit,后面接着的调用两次park(),在调用第一次park()时,就会把这个Permit消耗掉,第二次调用时就没有Permit了,因而会被阻塞。

    搞定( ̄∇ ̄)/🎉🎉🎉~~~~~~~~~~

  • 相关阅读:
    Linux网络命令使用简单说明
    深度学习-序列模型
    生命不息,运动不止,乐歌智能健身椅为健康加油助威
    「C++之STL」关于在模拟实现STL容器中的深浅拷贝问题
    虹科产品 | HK-ATTO 光纤通道卡利用FC-NVMe 提升全闪存存储阵列性能
    webrtc终极版(二)搭建自己的iceserver服务,并用到RTCMultiConnection的demo中
    如何使用C++图形界面开发框架Qt创建一个应用程序?(Part 3)
    计算机毕业设计Java-ssm藏宝阁游戏交易系统源码+系统+数据库+lw文档
    springboot集成Actuator+Prometheus+Grafana
    (附源码)springboot校园商铺系统 毕业设计 052145
  • 原文地址:https://blog.csdn.net/aqin1012/article/details/133748483