每日一博 - 闲聊 Java 中的中断

news2024/12/1 10:33:41

文章目录

  • 概述
  • 常见的中断问题
    • 中断一个处于运行状态的线程
    • 中断一个正在 sleep 的线程
    • 中断一个由于获取 ReentrantLock 锁而被阻塞的线程
  • 如何正确地使用线程的中断标识
  • JDK 的线程池 ThreadPoolExecutor 内部是如何运用中断实现功能的
  • 小结

在这里插入图片描述


概述

在 Java 中,中断是一种线程协作方式 。

比如说,当线程 A 正在运行时,线程 B 可以通过中断线程 A,来指示线程 A 停止它正在执行的操作。但是线程 A 如何响应线程 B 的中断,是需要依靠线程 A 的代码处理逻辑来做决定的。


常见的中断问题

  • 首先,我们来看一下如何来中断一个线程,以及如何判断一个线程是否被中断了。

  • 接着,我们看下中断处于不同状态下的线程时,被中断的线程会做如何响应。 然后,我们学习如何正确地利用中断标识来处理中断

  • 最后,我们看一下 JDK 的线程池 ThreadPoolExecutor 内部是如何运用中断实现功能的

中断一个处于运行状态的线程

我们先来看下,如何中断一个线程,以及中断一个处于运行状态的线程后,这个线程会做出什么反应呢?

public class InterruptDemo {
  public static void main(String[] argc) throws InterruptedException {
    //1. 创建子线程
    Thread threadOne = new Thread(new Runnable() {
      @Override
      public void run() {
        for (; ; ) {
          System.out.println("im threadOne thread:" + Thread.currentThread().isInterrupted());
        }
      }
    }, "THREAD-ONE");
    //2.启动线程
    threadOne.start();
    //3. main线程休眠1s
    Thread.sleep(1000);
    //4. 中断子线程
    threadOne.interrupt();
  }
}

在这段代码中,我们首先创建了一个名为“THREAD-ONE”的线程。线程所做的事情很简单,就是打印一行文本。然后,我们启动这个线程。

接着,我们让 main 线程休眠 1s,这是为了让创建的子线程可以在被中断前,可以打印子线程的中断标识。
最后,我们调用子线程的 interrupt() 方法来中断子线程。

运行这段代码会发现,在代码 4 执行之前,子线程会一直输出:im threadOne thread:false。代码 4 执行完毕后,子线程会一直运行,并且一直输出:im threadOne thread:true

简单来说,我们可以通过调用线程的 interrupt() 方法来中断某个线程。中断处于运行状态的线程并不会对它造成影响,中断线程仅仅是把被中断的线程的标志设置为了 true。另外,我们可以调用线程的 isInterrupted() 方法,来判断线程是否被中断了


中断一个正在 sleep 的线程

中断处于运行状态的线程不会有影响,那中断一个正在 sleep 的线程,会对这个线程产生什么影响呢?我们再来看一个案例

public class InterruptSleepDemo {
  public static void main(String[] argc) throws InterruptedException {
    //1. 创建子线程
    Thread threadOne = new Thread(new Runnable() {
      @Override
      public void run() {
        //1.1
        System.out.println("sub thread begin run");
        try {
          //1.2
          Thread.sleep(1000);
        } catch (InterruptedException e) {
         //1.3
          e.printStackTrace();
        }
        //1.4
        System.out.println("sub thread end run");
      }
    }, "THREAD-ONE");
    //2.启动线程
    threadOne.start();
    //3. main线程休眠100ms
    Thread.sleep(100);
    //4. 中断子线程
    threadOne.interrupt();
    System.out.println("threadOne already interrupted");
  }
}

我们先创建一个名称为“THREAD-ONE”线程,这个线程内部调用 Thread.sleep(1000) 来让自己休眠 1s,然后启动这个线程,开始运行。

接着,我们让 main 线程休眠 100ms,为的是在 main 线程执行代码 4 前,先让子线程执行代码 1.2 Thread.sleep(1000),并让子线程处于 TIMED_WAITING 状态。代码 4 则会调用子线程的 interrupt() 方法,来中断子线程。

运行上面这段代码,我们会看到这样的运行结果:

java.lang.InterruptedException: sleep interrupted
  at java.lang.Thread.sleep(Native Method)
  at org.example.InterruptSleepDemo$1.run(InterruptSleepDemo.java:12)
  at java.lang.Thread.run(Thread.java:748)

可以看到,当 main 线程执行完代码 4,中断了子线程后,子线程会在代码 1.2 的地方抛出 InterruptedException 异常。然后,代码 1.3 会捕获到这个异常并打印异常信息,最后执行代码 1.4,并退出线程的执行,这时线程就处于终止状态了。

总的来说,中断一个由于调用 Thread.sleep() 方法而处于 TIMED_WAITING 状态的线程,会导致被中断的线程抛出 InterruptedException 异常


中断一个由于获取 ReentrantLock 锁而被阻塞的线程

当中断一个由于获取 ReentrantLock 锁而被阻塞的线程,会产生什么效果呢?我们来看一下这段代码示例:

public class InterruptLockDemo {

  //0.创建独占锁
  private final static ReentrantLock LOCK = new ReentrantLock();

  public static void main(String[] argc) throws InterruptedException {
    //1. 创建子线程
    Thread threadOne = new Thread(new Runnable() {
      @Override
      public void run() {
        //1.1
        System.out.println("sub thread begin run");
        try {
          //1.2
          LOCK.lockInterruptibly();
          System.out.println("sub thread got lock");
        } catch (InterruptedException e) {
          //1.3
          e.printStackTrace();
        } finally {
          //1.4
          LOCK.unlock();
        }
        System.out.println("sub thread end run");
      }
    }, "THREAD-ONE");
    //2. main线程获取锁
    LOCK.lock();
    //3.启动线程
    threadOne.start();
    //4. main线程休眠100ms
    Thread.sleep(100);
    //5. 中断子线程
    threadOne.interrupt();
    //6. main线程释放锁
    LOCK.unlock();
  }
}

我们先创建一个独占锁,再创建一个名为“THREAD-ONE”的子线程,然后让 main 线程获取到这个独占锁,启动并运行子线程。让 main 线程休眠 100ms,是为了保证代码 1.2 的执行发生在中断子线程之前。

子线程执行到代码 1.2 时,发现锁已经被其他线程持有了,就会处于阻塞状态。当 main 线程执行到中断子线程代码时,子线程就会从阻塞状态返回,然后抛出 InterruptedException 异常。

运行上面这段代码,我们会得到如下所示的结果:

java.lang.InterruptedException
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
  at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
  at org.example.InterruptLockDemo$1.run(InterruptLockDemo.java:18)
  at java.lang.Thread.run(Thread.java:748)
Exception in thread "THREAD-ONE" java.lang.IllegalMonitorStateException
  at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
  at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
  at org.example.InterruptLockDemo$1.run(InterruptLockDemo.java:25)
  at java.lang.Thread.run(Thread.java:748)

综上所述,当中断一个由于使用 lockInterruptibly() 方法获取锁而阻塞的线程时,这个线程会从阻塞状态返回,然后会抛出 InterruptedException 异常


如何正确地使用线程的中断标识

说完这几种常见线程的中断案例,我们再来看一看,如何正确地使用线程的中断标识,来让被中断的线程正常退出执行呢

public class UseInterruptDemo {
  public static void main(String[] argc) throws InterruptedException {
    //1. 创建子线程
    Thread threadOne = new Thread(new Runnable() {
      @Override
      public void run() {
       // 1.1
        for (; !Thread.currentThread().isInterrupted(); ) {
          System.out.println("---do something");
        }
      }
    }, "THREAD-ONE");
    //2.启动线程
    threadOne.start();
    //3. main线程休眠1s
    Thread.sleep(1000);
    //4. 中断子线程
    threadOne.interrupt();
    System.out.println("threadOne already interrupted");
  }
}

在这段代码中,我们先创建一个线程,然后,启动并运行这个线程。让 main 线程休眠 1s,为的是让子线程的代码 1.1 可以打印数据,

接下来,调用子线程的 interrupt() 方法中断子线程。这时,子线程代码 1.1 的循环语句判断自己被中断了,就退出循环的执行,子线程也就结束运行了。

所以,中断处于运行状态的线程时,我们可以在被中断的线程内部判断当前线程的中断标识位是否被设置了,如果被设置了,就退出代码的执行,然后被中断的线程也就可以优雅地退出执行了


JDK 的线程池 ThreadPoolExecutor 内部是如何运用中断实现功能的

我们使用 ThreadPoolExecutor,在程序运行结束时,我们会调用它的 shutdown() 方法来关闭线程池。关闭线程池的其中一个步骤,就是中断当前不活跃的工作线程。

public void shutdown() {
     final ReentrantLock mainLock = this.mainLock;
     mainLock.lock();
     try {
         ...
         interruptIdleWorkers();
         ...
     } finally {
         mainLock.unlock();
     }
     tryTerminate();
 }
 private void interruptIdleWorkers(boolean onlyOne) {
     final ReentrantLock mainLock = this.mainLock;
     mainLock.lock();
     try {
         for (Worker w : workers) {
             Thread t = w.thread;
             if (!t.isInterrupted() && w.tryLock()) {
                 try {
                     t.interrupt(); // 中断线程
                 } catch (SecurityException ignore) {
                 } finally {
                     w.unlock();
                 }
             }
             if (onlyOne)
                 break;
         }
     } finally {
         mainLock.unlock();
     }
 } 

上面这段代码中,shutdown 方法内会调用 interruptIdleWorkers() 方法,来中断线程池中的空闲线程,interruptIdleWorkers() 内部会通过循环遍历所有的 Worker 线程,并且如果当前线程没被中断,则会中断当前线程。

那么,中断空闲线程会产生什么效果呢?我们需要看下线程池中工作线程的处理逻辑。

final void runWorker(Worker w) {
     ...
     try {
         // 如果获取不到任务,则退出循环
         while (task != null || (task = getTask()) != null) {
           ....
         }
         completedAbruptly = false;
     } finally {
         processWorkerExit(w, completedAbruptly);
     }
 }
 private Runnable getTask() {
     ...
     for (;;) {
         ...
         if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
             decrementWorkerCount();
             return null;
         }
         ...
         try {
             //
             Runnable r = timed ?
                 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                 workQueue.take();
             if (r != null)
                 return r;
             timedOut = true;
         } catch (InterruptedException retry) {
             timedOut = false;
         }
     }
 }

这段代码,在 runWorker() 方法内,工作线程会通过循环从线程池队列里面获取任务,如果获取到任务,工作线程就进行处理;如果获取不到,就退出执行。

这个 getTask() 方法是负责从线程池队列里面获取任务的。默认情况下,getTask() 方法会执行 workQueue.take(),从队列里面获取任务。如果当前队列没有任务,这个方法会阻塞,也就是这个工作线程就会被阻塞。

当其他线程调用线程池的 shutDown() 方法时,会中断阻塞到 workQueue.take() 方法的工作线程,然后这个工作线程就会从阻塞中返回,并抛出 InterruptedException 异常

异常被捕获后,getTask() 方法继续执行 for 循环,接着发现线程池已经关闭了,getTask() 就会返回 null。到此为止,当前工作线程就执行完毕了,就会被释放掉


小结

在这里插入图片描述

Java 中每个线程都有一个中断标识,用来标识当前线程是否被中断了。我们可以通过调用线程的 interrupt() 方法来中断一个线程,一个线程被中断后,它的中断标识就被设置为了 true,我们可以通过调用线程的 isInterrupted() 方法来判断这个线程是否被中断。

当我们中断一个处于运行状态的线程,比如线程正在执行计算,这时仅仅是把线程的中断标识设置为了 true,并不会对计算任务造成影响。

还有一类线程,因为调用了 Object 类的 wait()、wait(long) 或 wait(long, int) 方法或者 Thread 的 join()、join(long)、join(long, int),sleep(long)、 sleep(long, int) 方法而被阻塞。当我们中断这类线程时,被阻塞的线程会从阻塞状态返回,并抛出 InterruptedException 异常。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1051514.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

提升工作效率!如何巧用 Ansible 实现自动化运维?

Ansible 是一种开源的自动化运维工具,它基于 YAML 语言编写 playbook,可以用来自动化服务器的配置、部署和管理。 Ansible 使用 SSH 协议进行通信,可以在大量服务器上进行批量操作,无需在目标服务器上安装任何客户端软件。它支持…

ip的标准分类---分类的Ip

分类的 IP 即将 IP 地址划分为若干个固定类,每一类地址都由两个固定长度的字段组成。 其中第一个字段是网络号(net-id),它标志主机或路由器所连接的网络。一个网络号在整个因特网内必须是唯一的。 第二个字段是主机号&#xf…

Microsoft Office无法重装报错30015-44(3) 0-2031(17004)

1.问题描述 由于迁移文件夹导致Microsoft office软件无法使用,于是准备卸载重装,但是点击OfficeSetup.exe出现报错30015-44(3) 关闭后出现以下报错0-2031(17004) 2. 尝试的解决方式 重启后仍然无法解决问题 2.1 参考官网解决办法 手动从控制面板&…

Flutter笔记 - ListTile组件及其应用

Flutter笔记 ListTile组件及其应用 作者:李俊才 (jcLee95):https://blog.csdn.net/qq_28550263 邮箱 :291148484163.com 本文地址:https://blog.csdn.net/qq_28550263/article/details/133411883 目 录 1. …

王道考研计算机网络——传输层

一、传输层概述 复用:发送方不同的应用进程都可以使用同一个传输层的协议来传送数据 分用:接收方的传输层在去除报文段的首部之后能把数据交给正确的应用进程 熟知端口号就是知名端口号0-1023 客户端使用的端口号是动态变化的,不是唯一确定…

禾观科技采用亚马逊云科技的数据湖,实现数据化驱动运营的核心

如今,随着本土业务发展渐趋成熟,越来越多的中国企业开始依托跨境电商实现产品出口外销,大力拓展海外布局。在这一背景下,众多潜在机遇涌现,成为跨境电商“蓝海”的必争之地。 杭州禾观科技有限公司是面向全球新兴快时尚…

【JavaEE基础学习打卡08】JSP之初次认识say hello!

目录 前言一、JSP技术初识1.动态页面2.JSP是什么3.JSP特点有哪些 二、JSP运行环境配置1.JDK安装2.Tomcat安装 三、编写JSP1.我的第一个JSP2.JSP执行过程3.在IDEA中开发JSP 总结 前言 📜 本系列教程适用于JavaWeb初学者、爱好者,小白白。我们的天赋并不高…

排序:最佳归并树(优化外部排序中对磁盘的读写次数)

1.归并树的性质 每个初始归并段对应一个叶子结点,把归并段的块数作为叶子的权值归并树的WPL树中所有叶结点的带权路径长度之和归并过程中的磁盘I/O次数归并树的WPL*2 如下图: 每个初始归并段看作一个叶子结点,归并段的长度作为结点权值&a…

源码编译安装zstd

目录 1 下载源码https://github.com/facebook/zstd 2 解压 3 在解压后的目录里输入make 4 sudo make install 安装完毕 5 输入whereis zstd 检查安装结果 1 下载源码https://github.com/facebook/zstd 2 解压 3 在解压后的目录里输入make 4 sudo make install 安装完毕…

利用抽象工厂模式提升游戏开发的精度与灵活性

引言 大家好,我是亿元程序员,一位有着8年游戏行业经验的主程。 本系列是《和8年游戏主程一起学习设计模式》,让糟糕的代码在潜移默化中升华,欢迎大家关注分享收藏订阅。 在开发过程中,如何有效地管理各种游戏对象并…

UE4 Cesium 与ultra dynamic sky插件天气融合

晴天: 雨天: 雨天湿度: 小雪: 中雪: 找到该路径这个材质: 双击点开: 将Wet_Weather_Effects与Snow_Weather_Effects复制下来,包括参数节点 找到该路径这个材质,双击点开&…

Java基于 SpringBoot+Vue 的游戏分享网站

1 简介 基于Java SpringBoot 的游戏分享网站,本系统主要包括管理员和用户两个角色组成;主要包括首页、个人中心、用户管理、游戏类型管理、游戏文章管理、交流论坛、系统管理等功能的管理系统。 文章首发地址 2 技术栈 开发语言:Java 框…

【空间-光谱重构网络:高光谱和多光谱图像融合】

SSR-NET: Spatial–Spectral Reconstruction Network for Hyperspectral and Multispectral Image Fusion (SSR-NET:用于高光谱和多光谱图像融合的空间-光谱重构网络) 将低空间分辨率高光谱图像(LR-HSI)与高空间分辨…

数据结构与算法设计分析—— 数据结构及常用算法

目录 一、常用的数据结构(一)线性结构1、顺序表与链表2、栈3、队列 (二)非线性结构1、树与二叉树2、图3、集合 二、算法的基本概念(一)算法的特性(二)算法与数据结构 三、算法设计步…

嵌入式Linux应用开发-面向对象-分层-分离及总线驱动模型

嵌入式Linux应用开发-面向对象-分层-分离及总线驱动模型 第八章 驱动设计的思想:面向对象/分层/分离8.1 面向对象8.2 分层8.3 分离8.4 写示例代码8.5 课后作业 第九章 驱动进化之路:总线设备驱动模型9.1 驱动编写的 3种方法9.1.1 传统写法9.1.2 总线设备…

进阶指针(三)--- qsort函数(快速排序)的使用和(用冒泡排序)模拟实现

✨博客主页:小钱编程成长记 🎈博客专栏:进阶C语言 🎈推荐相关博文:进阶C语言(一)、进阶C语言(二) 进阶指针(三) 8.回调函数(下&#x…

​中秋团圆季《乡村振兴战略下传统村落文化旅游设计》许少辉八月新著

​中秋团圆季《乡村振兴战略下传统村落文化旅游设计》许少辉八月新著 ​中秋团圆季《乡村振兴战略下传统村落文化旅游设计》许少辉八月新著

手动实现BERT

本文重点介绍了如何从零训练一个BERT模型的过程,包括整体上BERT模型架构、数据集如何做预处理、MASK替换策略、训练模型和保存、加载模型和测试等。 一.BERT架构   BERT设计初衷是作为一个通用的backbone,然后在下游接入各种任务,包括翻译…

Azure Arc 概要:功能、管理和应用场景详解,AZ900 考点示例

文章目录 本文大纲一、什么是 Azure Arc二、使用 Azure Arc 可以做什么操作三、使用 Azure Arc 可以管理什么资源3.1 如何使用Azure Arc与服务器? 四、Azure Arc 支持的主要场景五、在 AZ900 中的考点示例5.1 示例题 15.2 示例题 2 本文大纲 本文思维导图概述的主要内容&…

【C/C++】C/C++面试八股

C/C面试八股 C和C语言的区别简单介绍一下三大特性多态的实现原理虚函数的构成原理虚函数的调用原理虚表指针在什么地方进行初始化的?构造函数为什么不能是虚函数虚函数和纯虚函数的区别抽象类类对象的对象模型内存对齐是什么?为什么要内存对齐static关键…