Java协作中断机制(一)

简介: Java协作中断机制(一)

1. 引言

当我们点击某个杀毒软件的取消按钮来停止查杀病毒时,当我们在控制台敲入 quit 命令以结束某个后台服务时……都需要通过一个线程去取消另一个线程正在执行的任务。Java 没有提供一种安全直接的方法来停止某个线程,但是 Java 提供了中断机制。

如果对 Java 中断没有一个全面的了解,可能会误以为被中断的线程将立马退出运行,但事实并非如此。中断机制是如何工作的?捕获或检测到中断后,是抛出 InterruptedException 还是重设中断状态以及在方法中吞掉中断状态会有什么后果?Thread.stop 与中断相比又有哪些异同?什么情况下需要使用中断?本文将从以上几个方面进行描述。


2. 中断的原理

Java 中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。这好比是家里的父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。

Java 中断模型也是这么简单,每个线程对象里都有一个 boolean 类型的标识(不一定就要是 Thread 类的字段,实际上也的确不是,这几个方法最终都是通过 native 方法来完成的),代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身)。例如,当线程 t1 想中断线程 t2,只需要在线程 t1 中将线程 t2 对象的中断标识置为 true,然后线程 2 可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。

java.lang.Thread 类提供了几个方法来操作这个中断状态,这些方法包括:

image.png

其中,interrupt 方法是唯一能将中断状态设置为 true 的方法。静态方法 interrupted 会将当前线程的中断状态清除,但这个方法的命名极不直观,很容易造成误解,需要特别注意。

上面的例子中,线程 t1 通过调用 interrupt 方法将线程 t2 的中断状态置为 true,t2 可以在合适的时候调用 interrupted 或 isInterrupted 来检测状态并做相应的处理。

此外,类库中的有些类的方法也可能会调用中断,如 FutureTask 中的 cancel 方法,如果传入的参数为 true,它将会在正在运行异步任务的线程上调用 interrupt 方法,如果正在执行的异步任务中的代码没有对中断做出响应,那么 cancel 方法中的参数将不会起到什么效果;又如 ThreadPoolExecutor 中的 shutdownNow 方法会遍历线程池中的工作线程并调用线程的 interrupt 方法来中断线程,所以如果工作线程中正在执行的任务没有对中断做出响应,任务将一直执行直到正常结束。


3. 中断的处理

既然 Java 中断机制只是设置被中断线程的中断状态,那么被中断线程该做些什么?

处理时机

显然,作为一种协作机制,不会强求被中断线程一定要在某个点进行处理。实际上,被中断线程只需在合适的时候处理即可,如果没有合适的时间点,甚至可以不处理,这时候在任务处理层面,就跟没有调用中断方法一样。“合适的时候”与线程正在处理的业务逻辑紧密相关,例如,每次迭代的时候,进入一个可能阻塞且无法中断的方法之前等,但多半不会出现在某个临界区更新另一个对象状态的时候,因为这可能会导致对象处于不一致状态。

处理时机决定着程序的效率与中断响应的灵敏性。频繁的检查中断状态可能会使程序执行效率下降,相反,检查的较少可能使中断请求得不到及时响应。如果发出中断请求之后,被中断的线程继续执行一段时间不会给系统带来灾难,那么就可以将中断处理放到方便检查中断,同时又能从一定程度上保证响应灵敏度的地方。当程序的性能指标比较关键时,可能需要建立一个测试模型来分析最佳的中断检测点,以平衡性能和响应灵敏性。


处理方式

1、 中断状态的管理

一般说来,当可能阻塞的方法声明中有抛出 InterruptedException 则暗示该方法是可中断的,如 BlockingQueue#put、BlockingQueue#take、Object#wait、Thread#sleep 等,如果程序捕获到这些可中断的阻塞方法抛出的 InterruptedException 或检测到中断后,这些中断信息该如何处理?一般有以下两个通用原则:

  • 如果遇到的是可中断的阻塞方法抛出 InterruptedException,可以继续向方法调用栈的上层抛出该异常,如果是检测到中断,则可清除中断状态并抛出 InterruptedException,使当前方法也成为一个可中断的方法。
  • 若有时候不太方便在方法上抛出 InterruptedException,比如要实现的某个接口中的方法签名上没有 throws InterruptedException,这时就可以捕获可中断方法的 InterruptedException 并通过 Thread.currentThread.interrupt() 来重新设置中断状态。如果是检测并清除了中断状态,亦是如此。


一般的代码中,尤其是作为一个基础类库时,绝不应当吞掉中断,即捕获到 InterruptedException 后在 catch 里什么也不做,清除中断状态后又不重设中断状态也不抛出 InterruptedException 等。因为吞掉中断状态会导致方法调用栈的上层得不到这些信息。

当然,凡事总有例外的时候,当你完全清楚自己的方法会被谁调用,而调用者也不会因为中断被吞掉了而遇到麻烦,就可以这么做。

总得来说,就是要让方法调用栈的上层获知中断的发生。假设你写了一个类库,类库里有个方法 amethod,在 amethod 中检测并清除了中断状态,而没有抛出 InterruptedException,作为 amethod 的用户来说,他并不知道里面的细节,如果用户在调用 amethod 后也要使用中断来做些事情,那么在调用 amethod 之后他将永远也检测不到中断了,因为中断信息已经被 amethod 清除掉了。如果作为用户,遇到这样有问题的类库,又不能修改代码,那该怎么处理?只好在自己的类里设置一个自己的中断状态,在调用 interrupt 方法的时候,同时设置该状态,这实在是无路可走时才使用的方法。


2、 中断的响应

程序里发现中断后该怎么响应?这就得视实际情况而定了。有些程序可能一检测到中断就立马将线程终止,有些可能是退出当前执行的任务,继续执行下一个任务……作为一种协作机制,这要与中断方协商好,当调用 interrupt 会发生些什么都是事先知道的,如做一些事务回滚操作,一些清理工作,一些补偿操作等。若不确定调用某个线程的 interrupt 后该线程会做出什么样的响应,那就不应当中断该线程。


4. Thread.interrupt VS Thread.stop

Thread.stop 方法已经不推荐使用了。而在某些方面 Thread.stop 与中断机制有着相似之处。如当线程在等待内置锁或 IO 时,stop 跟 interrupt 一样,不会中止这些操作;当 catch 住 stop 导致的异常时,程序也可以继续执行,虽然 stop 本意是要停止线程,这么做会让程序行为变得更加混乱。

那么它们的区别在哪里?最重要的就是中断需要程序自己去检测然后做相应的处理,而 Thread.stop 会直接在代码执行过程中抛出 ThreadDeath 错误,这是一个 java.lang.Error 的子类。

在继续之前,先来看个小例子:

package com.ticmy.interrupt;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class TestStop {
  private static final int[] array = new int[80000];
  private static final Thread t = new Thread() {
    public void run() {
      try {
        System.out.println(sort(array));
      } catch (Error err) {
        err.printStackTrace();
      }
      System.out.println("in thread t");
    }
  };
  static {
    Random random = new Random();
    for(int i = 0; i < array.length; i++) {
      array[i] = random.nextInt(i + 1);
    }
  }
  private static int sort(int[] array) {
    for (int i = 0; i < array.length-1; i++){
      for(int j = 0 ;j < array.length - i - 1; j++){
        if(array[j] < array[j + 1]){
          int temp = array[j];
          array[j] = array[j + 1];
          array[j + 1] = temp;
        }
      }
    }
    return array[0];
  }
  public static void main(String[] args) throws Exception {
    t.start();
    TimeUnit.SECONDS.sleep(1);
    System.out.println("go to stop thread t");
    t.stop();
    System.out.println("finish main");
  }
}

这个例子很简单,线程 t 里面做了一个非常耗时的排序操作,排序方法中,只有简单的加、减、赋值、比较等操作,一个可能的执行结果如下:

go to stop thread t
java.lang.ThreadDeath
  at java.lang.Thread.stop(Thread.java:758)
  at com.ticmy.interrupt.TestStop.main(TestStop.java:44)
finish main
in thread t

这里 sort 方法是个非常耗时的操作,也就是说主线程休眠一秒钟后调用 stop 的时候,线程 t 还在执行 sort 方法。就是这样一个简单的方法,也会抛出错误!换一句话说,调用 stop 后,大部分 Java 字节码都有可能抛出错误,哪怕是简单的加法!

如果线程当前正持有锁,stop 之后则会释放该锁。由于此错误可能出现在很多地方,那么这就让编程人员防不胜防,极易造成对象状态的不一致。例如,对象 obj 中存放着一个范围值:最小值 low,最大值 high,且 low 不得大于 high,这种关系由锁 lock 保护,以避免并发时产生竞态条件而导致该关系失效。假设当前 low 值是 5,high 值是 10,当线程 t 获取 lock 后,将 low 值更新为了 15,此时被 stop 了,真是糟糕,如果没有捕获住 stop 导致的 Error,low 的值就为 15,high 还是 10,这导致它们之间的小于关系得不到保证,也就是对象状态被破坏了!如果在给 low 赋值的时候 catch 住 stop 导致的 Error 则可能使后面 high 变量的赋值继续,但是谁也不知道 Error 会在哪条语句抛出,如果对象状态之间的关系更复杂呢?这种方式几乎是无法维护的,太复杂了!如果是中断操作,它决计不会在执行 low 赋值的时候抛出错误,这样程序对于对象状态一致性就是可控的。

正是因为可能导致对象状态不一致,stop 才被禁用。


目录
相关文章
|
8天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
28 2
|
12天前
|
Java 编译器
探索Java中的异常处理机制
【10月更文挑战第35天】在Java的世界中,异常是程序运行过程中不可避免的一部分。本文将通过通俗易懂的语言和生动的比喻,带你了解Java中的异常处理机制,包括异常的类型、如何捕获和处理异常,以及如何在代码中有效地利用异常处理来提升程序的健壮性。让我们一起走进Java的异常世界,学习如何优雅地面对和解决问题吧!
|
1月前
|
存储 算法 Java
Java HashSet:底层工作原理与实现机制
本文介绍了Java中HashSet的工作原理,包括其基于HashMap实现的底层机制。通过示例代码展示了HashSet如何添加元素,并解析了add方法的具体过程,包括计算hash值、处理碰撞及扩容机制。
|
23天前
|
XML 安全 Java
Java反射机制:解锁代码的无限可能
Java 反射(Reflection)是Java 的特征之一,它允许程序在运行时动态地访问和操作类的信息,包括类的属性、方法和构造函数。 反射机制能够使程序具备更大的灵活性和扩展性
35 5
Java反射机制:解锁代码的无限可能
|
11天前
|
Java 数据库连接 开发者
Java中的异常处理机制及其最佳实践####
在本文中,我们将探讨Java编程语言中的异常处理机制。通过深入分析try-catch语句、throws关键字以及自定义异常的创建与使用,我们旨在揭示如何有效地管理和响应程序运行中的错误和异常情况。此外,本文还将讨论一些最佳实践,以帮助开发者编写更加健壮和易于维护的代码。 ####
|
17天前
|
安全 IDE Java
Java反射Reflect机制详解
Java反射(Reflection)机制是Java语言的重要特性之一,允许程序在运行时动态地获取类的信息,并对类进行操作,如创建实例、调用方法、访问字段等。反射机制极大地提高了Java程序的灵活性和动态性,但也带来了性能和安全方面的挑战。本文将详细介绍Java反射机制的基本概念、常用操作、应用场景以及其优缺点。 ## 基本概念 ### 什么是反射 反射是一种在程序运行时动态获取类的信息,并对类进行操作的机制。通过反射,程序可以在运行时获得类的字段、方法、构造函数等信息,并可以动态调用方法、创建实例和访问字段。 ### 反射的核心类 Java反射机制主要由以下几个类和接口组成,这些类
36 2
|
22天前
|
存储 缓存 安全
🌟Java零基础:深入解析Java序列化机制
【10月更文挑战第20天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
23 3
|
22天前
|
安全 Java UED
深入理解Java中的异常处理机制
【10月更文挑战第25天】在编程世界中,错误和意外是不可避免的。Java作为一种广泛使用的编程语言,其异常处理机制是确保程序健壮性和可靠性的关键。本文通过浅显易懂的语言和实际示例,引导读者了解Java异常处理的基本概念、分类以及如何有效地使用try-catch-finally语句来处理异常情况。我们将从一个简单的例子开始,逐步深入到异常处理的最佳实践,旨在帮助初学者和有经验的开发者更好地掌握这一重要技能。
20 2
|
24天前
|
Java 数据库连接 开发者
Java中的异常处理机制####
本文深入探讨了Java语言中异常处理的核心概念,通过实例解析了try-catch语句的工作原理,并讨论了finally块和throws关键字的使用场景。我们将了解如何在Java程序中有效地管理错误,提高代码的健壮性和可维护性。 ####
|
27天前
|
安全 Java 程序员
深入浅出Java中的异常处理机制
【10月更文挑战第20天】本文将带你一探Java的异常处理世界,通过浅显易懂的语言和生动的比喻,让你在轻松阅读中掌握Java异常处理的核心概念。我们将一起学习如何优雅地处理代码中不可预见的错误,确保程序的健壮性和稳定性。准备好了吗?让我们一起踏上这段旅程吧!
25 6