多线程-interrupt(),isInterrupted(),interrupted()
背景
- 由于使用stop方法停止线程非常暴力,可能会导致一系列问题。因此,提出一种温和的方式:请求另外一个先不要在执行了,这就是中断方式。
- 此外有这样的场景:编写 一个程序,需要暂停一段时间,于是调用Thread.sleep(),但是编译器或IDE报错说没有处理检查到的InterruptedException。对于InterruptedException,一种常见的处理方式是“生吞”它:捕获它,然后什么也不做。
- 在某个子线程中为了等待一些特定条件的到来, 你调用了Thread.sleep(10000), 预期线程睡10秒之后自己醒来, 但是如果这个特定条件提前到来的话, 来通知一个处于Sleep的线程。又比如说线程通过调用子线程的join方法阻塞自己以等待子线程结束, 但是子线程运行过程中发现自己没办法在短时间内结束, 于是它需要想办法告诉主线程别等我了。 这些情况下,,就需要中断。
- 在 java中启动线程非常容易,大多数情况下我是让一个线程执行完自己的任务然后自己停掉,但是有时候我们需要取消某个操作,比如你在网络下载时,有时候需要取消下载。实现线程的安全中断并不是一件容易的事情,因为Java并不支持安全快速中断线程的机制。
中断
每个线程都有一个与之相关联的 Boolean 属性,用于表示线程的中断状态(interrupted status)。中断状态初始时为 false;当另一个线程通过调用Thread.interrupt()中断一个线程时,会出现以下两种情况之一。(1)如果那个线程在执行一个低级可中断阻塞方法,例如Thread.sleep(), Thread.join()或 Object.wait(),那么它将取消阻塞并抛出InterruptedException。(2)否则,interrupt() 只是设置线程的中断状态。在被中断线程中运行的代码以后可以轮询中断状态,看看它是否被请求停止正在做的事情。中断状态可以通过 Thread.isInterrupted()来读取,并且可以通过一个名为Thread.interrupted() 的操作读取和清除。
中断是一种协作机制。当一个线程中断另一个线程时,被中断的线程不一定要立即停止正在做的事情。相反,中断是礼貌地请求另一个线程在它愿意并且方便的时候停止它正在做的事情。有些方法,例如Thread.sleep(),很认真地对待这样的请求,但每个方法不是一定要对中断作出响应。对于中断请求,不阻塞但是仍然要花较长时间执行的方法可以轮询中断状态,并在被中断的时候提前返回。 您可以随意忽略中断请求,但是这样做的话会影响响应。
中断的协作特性所带来的一个好处是,它为安全地构造可取消活动提供更大的灵活性。我们很少希望一个活动立即停止;如果活动正在进行更新的时候被取消,那么程序数据结构可能处于不一致状态。中断允许一个可取消活动来清理正在进行的工作,恢复不变量,通知其他活动它要被取消,然后才终止。
相关方法
this.interrupt()
中断调用该方法的线程,除非当前线程正在中断自己,否则checkAccess方法有可能抛出SecurityException异常。
(1)如果当前线程由于调用Object的wait(),或者Thread的join和sleep方法阻塞,则退出阻塞且中断状态将被清除,并且抛出InterrruptedException异常。
(2)如果线程由于InterruptibleChannel的IO操作阻塞,则通道将关闭,线程设置中断状态,并且线程收到一个ClosedInterruptException异常。
(3)如果线程由于Selector阻塞,线程将处于中断状态,并且从selection操作立刻返回,可能是一个非零值。
除此之外,线程将被设置中断状态。
总的来说:interrupt方法有两个作用:(1)将线程的中断状态设置为true(2)让被阻塞的线程抛出InterruptedException异常(同时中断状态为false)。
这样,对于那些阻塞方法(比如 wait() 和 sleep())而言,当另一个线程调用interrupt()中断该线程时,该线程会从阻塞状态退出并且抛出中断异常。这样,我们就可以捕捉到中断异常,并根据实际情况对该线程从阻塞方法中异常退出而进行一些处理。
注意:没有占用CPU运行的线程是不可能给自己的中断状态置位的,就会产生一个InterruptedException异常。
比如说:线程A获得了锁进入了同步代码块中,但由于条件不足调用 wait() 方法阻塞了。这个时候,线程B执行 threadA.interrupt()请求中断线程A,此时线程A就会抛出InterruptedException,我们就可以在catch中捕获到这个异常并进行相应处理(比如进一步往上抛出)
this.isInterrupted()
检测调用该方法的线程是否被中断,中断状态不会被清除。线程一旦被中断,该方法返回true,而一旦sleep等方法抛出异常,它将清除中断状态,此时方法将返回false。
package com.huawei.thread; public class Test30 { public static void main(String[] args) { Thread t = Thread.currentThread(); System.out.println("1: " + t.isInterrupted()); t.interrupt(); System.out.println("2: " + t.isInterrupted()); System.out.println("3: " + t.isInterrupted()); try { Thread.sleep(2000); System.out.println("not interrted..."); } catch (Exception e) { System.out.println("interrupted..."); System.out.println("4: " + t.isInterrupted()); } System.out.println("5: " + t.isInterrupted()); } }
Thread.interrupted()
检测当前线程是否被中断,并且中断状态会被清除(即重置为false);由于它是静态方法,因此不能在特定的线程上使用,只能报告调用它的线程的中断状态;如果该方法被调用两次,则第二次一般是返回false,如果线程不存活,则返回false。
package com.huawei.thread; public class Test31 { public static void main(String[] args) { System.out.println("1: " + Thread.interrupted()); Thread.currentThread().interrupt(); System.out.println("2: " + Thread.interrupted()); System.out.println("3: " + Thread.interrupted()); } }
阻塞方法
当一个方法抛出InterruptedException时,它不仅告诉你它可以抛出一个特定的检查异常,而且还告诉你其他一些事情。例如,它告诉你它是一个阻塞(blocking)方法,如果你响应得当的话,它将尝试消除阻塞并尽早返回。
阻塞方法不同于一般的要运行较长时间的方法。一般方法的完成只取决于它所要做的事情,以及是否有足够多可用的计算资源(CPU 周期和内存)。而阻塞方法的完成还取决于一些外部的事件,例如计时器到期,I/O 完成,或者另一个线程的动作(释放一个锁,设置一个标志,或者将一个任务放在一个工作队列中)。一般方法在它们的工作做完后即可结束,而阻塞方法较难于预测,因为它们取决于外部事件。阻塞方法可能影响响应能力,因为难于预测它们何时会结束。
阻塞方法可能因为等不到所等的事件而无法终止,因此令阻塞方法可取消就非常有用(如果长时间运行的非阻塞方法是可取消的,那么通常也非常有用)。可取消操作是指能从外部使之在正常完成之前终止的操作。由Thread提供并受Thread.sleep()和Object.wait()支持的中断机制就是一种取消机制;它允许一个线程请求另一个线程停止它正在做的事情。当一个方法抛出InterruptedException时,它是在告诉你,如果执行该方法的线程被中断,它将尝试停止它正在做的事情而提前返回,并通过抛出InterruptedException表明它提前返回。行为良好的阻塞库方法应该能对中断作出响应并抛出InterruptedException,以便能够用于可取消活动中,而不至于影响响应。
不可中断的阻塞方法
并非所有的阻塞方法都抛出InterruptedException。输入和输出流类会阻塞等待 I/O 完成,但是它们不抛出InterruptedException,而且在被中断的情况下也不会提前返回。然而,对于套接字 I/O,如果一个线程关闭套接字,则那个套接字上的阻塞 I/O 操作将提前结束,并抛出一个SocketException。java.nio中的非阻塞 I/O 类也不支持可中断 I/O,但是同样可以通过关闭通道或者请求Selector上的唤醒来取消阻塞操作。类似地,尝试获取一个内部锁的操作(进入一个synchronized
块)是不能被中断的,但是ReentrantLock支持可中断的获取模式。
处理不支持中断的线程中断的常用方法
如果一个线程由于同步记性IO操作导致阻塞,中断请求不会抛出InterruptedException,该如何中断此线程呢?
package com.huawei.thread; import java.io.IOException; import java.io.InputStream; import java.net.Socket; public class Test32 extends Thread { public static final int BUF_SIZE = 512; Socket socket; InputStream in; public Test32(Socket socket) throws IOException { this.socket = socket; this.in = socket.getInputStream(); } @Override public void interrupt() { try { socket.close(); } catch (IOException e) { } finally { super.interrupt(); } } @Override public void run() { try { byte[] buf = new byte[BUF_SIZE]; while (true) { int count = in.read(buf); if (count < 0) { break; } else if (count > 0) { } } } catch (IOException e) { } } }
通过改写了Thread.interrupt方法,当调用interrupt方法时,会关闭socket,如果此时read方法阻塞,那么会抛出IOException异常,此时线程任务也就结束了。
处理InterruptedException
(1)如果抛出InterrruptedException意味着一个方法是阻塞方法,那么调用一个阻塞方法则意味着你的方法也是一个阻塞方法,应该有某种策略来处理InterrruptedException。通常最容易的策略是自己抛出InterrruptedException,这样做可以使方法对中断做出响应,而且只需将InterruptedException添加到throws子句。
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(); } }
(2)有时候需要在传播异常之前进行一些清理工作,在这种情况下,可以捕获InterruptedException,执行清理,然后抛出异常。
public class PlayerMatcher { private PlayerSource players; public PlayerMatcher(PlayerSource players) { this.players = players; } public void matchPlayers() throws InterruptedException { try { Player playerOne, playerTwo; 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; } } }
(3)不要生吞中断:有时候抛出InterruptedException并不合适,例如当由Runnable定义的任务调用一个可中断的方法时,在这种情况下,不能重新抛出InterruptedException,因为Runnable接口的run方法不允许抛出异常。当一个阻塞方法检测到中断并抛出InterruptedException但是不能重新抛出它,那么应该保留中断发生的证据,以便调用栈中更高层的代码能知道中断,并对中断做出响应,该任务可以通过调用interrupt()以重新中断当前线程来完成。
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(); } } }
待决中断
sleep等方法的实现检查到阻塞线程被中断,它会友好的终止线程,并抛出InterruptedException异常。另外一种情况,如果线程在调用sleep等方法之前被中断,那么该中断就称为:待决中断,它会在刚调用sleep等方法时,立刻抛出InterruptedException异常。
package com.huawei.thread; public class Test29 { public static void main(String[] args) { Thread.currentThread().interrupt(); long start = System.currentTimeMillis(); try { Thread.sleep(40000); System.out.println("not interrupted..."); } catch (Exception e) { System.out.println("interrupted..."); } System.out.println(System.currentTimeMillis() - start); } }
运行结果如下:
这种模式下,main线程中断它自身。除了将中断标志(它是Thread的内部标志)设置为true外,没有其他任何影响。线程被中断了,但main线程仍然运行,main线程继续监视实时时钟,并进入try块,一旦调用sleep()方法,它就会注意到待决中断的存在,并抛出InterruptException。于是执行跳转到catch块,并打印出线程被中断的信息。最后,计算并打印出时间差。
实例1
package com.huawei.thread; public class Test27 { public static void main(String[] args) { try { MyThread1 t = new MyThread1(); t.start(); Thread.sleep(2000); t.interrupt(); // 中断线程t } catch (InterruptedException e) { System.out.println("main catch"); e.printStackTrace(); } System.out.println("end main"); } } class MyThread1 extends Thread { @Override public void run() { for (int i = 1; i < 500000; i++) { if (Thread.interrupted()) { System.out.println("stop and exit..."); break; } System.out.println("i: " + i); } // 尽管线程被中断,但并没有结束运行,这行代码还是被执行。 System.out.println("end run..."); } }
运行结果部分截图:
当t获取CPU执行时,if判断中,检测到中断状态,即在main线程中调用的t.interrupt(),for循环执行break跳出for循环,但是线程并没有结束,还是继续执行for后面的语句。可以将break改成return,则线程立刻结束执行。
当然也可以抛出InterruptedException异常:
class MyThread1 extends Thread { @Override public void run() { try { for (int i = 1; i < 500000; i++) { if (Thread.interrupted()) { System.out.println("stop and exit..."); throw new InterruptedException(); } System.out.println("i: " + i); } System.out.println("end run..."); } catch (Exception e) { System.out.println("catch interrupted..."); } } }
当检测到中断标识为true,抛出异常,这样该线程就不会再执行其他的正常语句。
前面说过不能生吞中断,修改如下:
class MyThread1 extends Thread { @Override public void run() { try { for (int i = 1; i < 500000; i++) { if (Thread.interrupted()) { System.out.println("stop and exit..."); throw new InterruptedException(); } System.out.println("i: " + i); } System.out.println("end run..."); } catch (Exception e) { System.out.println("catch interrupted..."); Thread.currentThread().interrupt(); // 不生吞中断 } } }
这样,就由生吞异常,将异常进一步扩散了。
实例2
package com.huawei.thread; public class Test28 extends Thread { volatile Boolean stop = false; public static void main(String[] args) throws InterruptedException { Test28 t = new Test28(); System.out.println("starting thread..."); t.start(); Thread.sleep(3000); System.out.println("asking thread to stop..."); // 必须要在interrupt之前设置 // 如果线程阻塞,将不会检查此变量,调用interrupt之后,线程就可以尽早的终结被阻塞状态,能够检查这一变量 t.stop = true; // 如果线程没有被阻塞,这时调用interrupt将不起作用。 // 这一方法实际上完成的是:在线程受到阻塞时抛出一个中断信号,这样线程就可以退出阻塞状态 t.interrupt(); Thread.sleep(3000); System.out.println("stopping app..."); } @Override public void run() { while (!stop) { System.out.println("running..."); try { Thread.sleep(2000); } catch (InterruptedException e) { System.out.println("interrupted..."); } } System.out.println("thread exit..."); } }
把握几个重点:stop变量、run方法中的sleep()、interrupt()、InterruptedException。串接起来就是这个意思:
- 当我们在run方法中调用sleep(或其他阻塞线程的方法)时,如果线程阻塞的时间过长,比如10s,那在这10s内,线程阻塞,run方法不被执行;
- 但是如果在这10s内,stop被设置成true,表明要终止这个线程;但是,现在线程是阻塞的,它的run方法不能执行,自然也就不能检查stop,所以线程不能终止;
- 这个时候,我们就可以用interrupt()方法了:我们在thread.stop = true;语句后调用thread.interrupt()方法, 该方法将在线程阻塞时抛出一个中断信号,该信号将被catch语句捕获到,一旦捕获到这个信号,线程就提前终结自己的阻塞状态;
- 这样,它就能够 再次运行run 方法了,然后检查到stop = true,while循环就不会再被执行,在执行了while后面的清理工作之后,run方法执行完 毕,线程终止。
当代码调用中需要抛出一个InterruptedException,可以选择吧中断状态复位,也可以选在向外抛出InterruptedException,由外层的调用者来决定。
参考资料
https://www.ibm.com/developerworks/cn/java/j-jtp05236.html
http://www.cnblogs.com/hapjin/p/5450779.html
http://blog.csdn.net/canot/article/details/51087772
http://blog.csdn.net/ns_code/article/details/17091267
http://developer.51cto.com/art/201508/487231.htm