JAVA打断线程

2018年6月19日11:56:08 发表评论 388

线程中断

良好的线程终止并非暴力地立刻停止一切操作,而是提供一种灵活的协作机制,安全的施行终止任务,中断机制好处在于允许清理正在进行的任何工作,恢复状态、通知其他活动取消等操作,然后终止。

 

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

 

每个线程对象里都有一个boolean类型的属性(不是Thread类的字段,而是通过native方法来完成状态转化,初始值为false),代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身)。

 

当一个线程被其他线程调用Thread.interrupt()方法,将发生以下其中一种:

  • 若该线程正在执行Thread.sleep(),Thread.join()或Object.wait()等低级可中断阻塞方法,则它将取消阻塞并抛出InterruptedException异常,注意此种方式不能再Runnable接口的run()实现方法中使用。

  • Thread.interrupt()仅仅设置了线程的中断状态而对原来的运行毫无影响。

 

应该强调的是,在收到中断请求后的被中断线程并没有立即终止,何时结束取决于被中断线程的处理逻辑,你可以自由的忽略这些中断请求,但这样做会影响程序的响应能力。

 

 中断线程最好的,最受推荐的方式是,使用共享变量(shared​​ variable)发出信号,告诉线程必须停止正在运行的任务。线程必须周期性的核查这一变量(尤其在冗余操作期间),然后有秩序地中止任务。

请确认将共享变量定义成volatile​​ 类型或将对它的一切访问封入同步的块/方法(synchronized blocks/methods)中。

在任何一种情况中,最后线程都将检查共享变量然后再停止。

中断场景

  • 通过一个线程取消另外的线程时;

  • 点击桌面应用中的取消按钮时;

  • IO操作超过了一定的执行时间限制需要中止时;

  • 多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;

  • 一组线程中的一个或多个出现错误导致整组都无法继续时;

当一个应用或服务需要停止时。

中断方法

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

  • public static boolean interrupted()

测试当前线程是否已经中断,线程的中断状态由该方法清除。如果连续两次调用该方法,则第一次返回的结果为true,第二次调用将返回​​ false(第一次调用清除了它的中断状态),但这个方法的命名极不直观,很容易造成误解,需要特别注意。

  • public boolean isInterrupted()

线程是否已经中断。线程的中断状态不受该方法的影响。

  • public void interrupt()

中断线程,唯一能将中断状态设置为true的方法。

阻塞方法

若线程在阻塞状态时,调用了它的interrupt()方法,那么它的中断状态会被清除并且会收到一个InterruptedException异常。例如,线程通过wait()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该中断标记会立即被清除为“false”,同时,会产生一个InterruptedException的异常。

如果抛出InterruptedException意味着该方法是一个阻塞方法,那么调用一个阻塞方法意味着你的方法也是阻塞方法,并且你应该有一个处理InterruptedException的策略。​​ 通常最简单的策略是自己抛出InterruptedException。

public class TaskQueue {

 ​​ ​​ ​​​​ private static final int MAX_TASKS = 1000;

​​ 

 ​​ ​​ ​​​​ private BlockingQueue<Task> queue​​ 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ = new LinkedBlockingQueue<Task>(MAX_TASKS);

​​ 

 ​​ ​​ ​​​​ public void putTask(Task r)​​ throws InterruptedException​​ {​​ 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ queue.put(r);

 ​​ ​​ ​​​​ }

​​ 

 ​​ ​​ ​​​​ public Task getTask()​​ throws InterruptedException​​ {​​ 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ return queue.take();

 ​​ ​​ ​​​​ }

}

Java中的阻塞方法是那些阻塞正在执行的线程直到它们的操作完成的方法。典型的阻塞方法有:

  • InputStream.read()​​ 持续阻塞直到输入数据可用。

  • ServerSocket.accept() 持续监听直到connection连接被创建可用并绑定到Socket上。

  • InvokeAndWait()

  • Object.wait()​​ 阻塞当前线程,直到其他线程调用监视对象的Object.notify()或者Object.notifyAll()方法,或者达到指定时间自己解除​​ -​​ wait(long mills)

  • Thread.sleep(long mills)​​ 使当前正在执行的线程休眠,直到指定的时间。

  • Thread.join()​​ 阻塞当前线程,直到其他线程完成运行。

  • BlockingQueue​​ and​​ BlockingDeque​​ 接口

处理方式

  • 不要轻易吞掉异常信息,捕获异常并记录它,甚至什么也不做都属于吞没异常,这会影响程序取消操作以及关闭的时机。

// Don't do this​​ 

public class TaskRunner implements Runnable {

 ​​ ​​ ​​​​ private BlockingQueue<Task> queue;

​​ 

 ​​ ​​ ​​​​ public TaskRunner(BlockingQueue<Task> queue) {​​ 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ this.queue = queue;​​ 

 ​​ ​​ ​​​​ }

​​ 

 ​​ ​​ ​​​​ public void run() {​​ 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ try {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ while (true) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ Task task = queue.take(10, TimeUnit.SECONDS);

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ task.execute();

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ catch (InterruptedException swallowed) {​​ 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ ​​ /* DON'T DO​​ THIS - RESTORE THE INTERRUPTED STATUS INSTEAD */

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​​​ }

}

  • 良好的阻塞方法,应该在传播异常之前对中断做出响应,有时需要做一些清理工作,然后捕获并重新抛出InterruptedException异常,以便它们可用于取消操作而不会影响响应性。

public class PlayerMatcher {

 ​​ ​​ ​​​​ private PlayerSource players;

​​ 

 ​​ ​​ ​​​​ public PlayerMatcher(PlayerSource players) {​​ 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ this.players = players;​​ 

 ​​ ​​ ​​​​ }

​​ 

 ​​ ​​ ​​​​ public void matchPlayers() throws InterruptedException {​​ 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ Player playerOne, playerTwo;

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ try {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ while (true) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ playerOne = playerTwo = null;

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ // Wait for two players to arrive and start a new game

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ playerOne = players.waitForPlayer(); // could throw IE

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ playerTwo = players.waitForPlayer(); // could throw IE

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ startNewGame(playerOne,​​ playerTwo);

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ catch (InterruptedException e) { ​​ 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ // If we got one player and were interrupted, put that player back

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ if (playerOne != null)

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ players.addFirst(playerOne);

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​  ​​​​ // Then propagate the exception

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ throw e;

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​​​ }

}

  • 无法抛出InterruptedException异常的时候,如Runnable定义的任务调用可中断方法时,无法重新抛出InterruptedException【run()方法的局限】,这时应该保留证据表明发生了中断,比如在返回之前“重新中断”当前线程,以便调用堆栈上的较高代码可以了解中断信息并根据需要做出响应。

public class TaskRunner​​ implements Runnable {

    private BlockingQueue<Task> queue;

 

    public TaskRunner(BlockingQueue<Task> queue) {​​ 

        this.queue = queue;​​ 

    }

 

    public void run() {​​ 

        try {

             while (true) {

                 Task task =​​ queue.take(10, TimeUnit.SECONDS);

                 task.execute();

             }

         }

         catch (InterruptedException e) {​​ 

             // Restore the interrupted status

            ​​ Thread.currentThread().interrupt();

         }

    }

}

  • 使用Thread.isInterrupted()读取被中断的状态,也可以通过Thread.interrupted()一次性读取并清除被中断状态。

  • InterruptedException捕获一般放在while(true)循环体的外面,这样在产生异常时就退出了while(true)循环。否则,InterruptedExceptionwhile(true)循环体之内,当产生InterruptedException异常时,被catch处理之外,仍然在while(true)循环体内,就需要额外的添加退出处理,​​ 比如添加break​​ ​​ return语句

// Better

@Override

public void run() {

 ​​ ​​ ​​​​ try {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ while (true) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ //​​ 执行任务...

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​​​ } catch (InterruptedException ie) { ​​ 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ //​​ 由于产生InterruptedException异常,退出while(true)循环,线程终止!

 ​​ ​​ ​​​​ }

}

 

@Override

public​​ void run() {

 ​​ ​​ ​​​​ while (true) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ try {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ //​​ 执行任务...

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ } catch (InterruptedException ie) { ​​ 

 ​​ ​​​​  ​​   ​​ ​​​​ // InterruptedException在while(true)循环体内。

 ​​ ​​​​  ​​   ​​ ​​​​ //​​ 当线程产生了InterruptedException异常时,

// while(true)仍能继续运行,需要手动退出。

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ break;

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​​​ }

}

  • 对于非阻塞的方法,在中断线程中运行的代码可以轮询中断状态,以便查看是否有请求停止正在执行的操作;​​ 我们通过标记方式终止处于运行状态的线程。其中,包括中断标记额外添加标记​​ interrupt()并不会终止处于运行状态的线程!它会将线程的中断标记设为true

//​​ 中断标记

//​​ 在Thread子类中调用isInterrupted()方法判断

@Override

public void run() {

 ​​ ​​ ​​​​ while​​ (!isInterrupted()) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ //​​ 执行任务...

 ​​ ​​ ​​​​ }

}

 

说明:(下面代码)

线程中有一个flag标记,它的默认值是true;并且我们提供stopTask()来设置flag标记。当我们需要终止该线程时,调用该线程的stopTask()方法就可以让线程退出while循环。

注意:

flag定义为volatile类型,是为了保证flag的可见性。即其它线程通过stopTask()修改了flag之后,本线程能看到修改后的flag的值。

//​​ 额外添加标记

private​​ volatile​​ boolean flag = true;

protected void stopTask() {

 ​​ ​​ ​​​​ flag = false;

}

 

@Override

public void run() {

 ​​ ​​ ​​​​ while (flag) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ //​​ 执行任务...

 ​​ ​​ ​​​​ }

}

 

  • 通用的终止线程的形式

@Override

public void run() {

 ​​ ​​ ​​​​ try {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ // 1. isInterrupted()保证,只要中断标记为true就终止线程。

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ while (!isInterrupted()) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ //​​ 执行任务...

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​​​ } catch (InterruptedException ie) { ​​ 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ // 2. InterruptedException异常保证,

//​​ 当InterruptedException异常产生时,线程被终止。

 ​​ ​​ ​​​​ }

}

 

  • InputStreamread调用,该操作是不可中断的,如果流中没有数据,read会阻塞​​ (但线程状态依然是RUNNABLE),且不响应interrupt(),与synchronized类似,调用interrupt()只会设置线程的中断标志,而不会真正中断

处理时机

  • 合适的时候与线程正在处理的业务逻辑紧密相关;

  • 中断作为一种协作机制,不会强求被中断线程一定要在某个点进行处理;

  • 实际上,被中断线程只需在合适的时候处理即可,如果没有合适的时间点,甚至可以不处理,这时候在任务处理层面,就跟没有调用中断方法一样。

  • 处理时机决定着程序的效率与中断响应的灵敏性。

    • 频繁的检查中断状态可能会使程序执行效率下降;

    • 相反,检查的较少可能使中断请求得不到及时响应。

    • 如果发出中断请求之后,被中断的线程继续执行一段时间不会给系统带来灾难,那么就可以将中断处理放到方便检查,同时又能从一定程度上保证响应灵敏度的地方。

    • 当程序的性能指标比较关键时,可能需要建立一个测试模型来分析最佳的中断检测点,以平衡性能和响应灵敏性。

 

 

weinxin
微信公众号
分享IT信息技术、北海生活的网站。提供北海本地化的信息技术服务。
连线北海

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: