摘要
在java中,想要让一个线程停下来,有三种办法:
(1)采用退出标志,使得run方法执行完之后线程自然终止。
(2)使用stop强行终止线程,但该方法由于安全问题已经被deprecated。
(3)使用中断机制。
引入
第一种方法没特别之处,无非是在覆盖Runnable接口之时对run方法中添加状态标识逻辑。比如:
public class MyThread extends Thread
{
private boolean running;
@Override
public run(){
if(running){
//....business methods
}
}
}
下面详细讨论中断机制。
这里的中断是软中断,即在某个线程中可以将目标线程的中断标志位置位,然后在目标线程进行中断检查的时候来判断自己应当如何响应中断。
特别的,可以以此来停止目标线程。中断的局限性,无法对出于死锁状态的线程起作用。
简单原理:
java.lang.Thread
类中提供了一个方法来将线程的中断标志位置位:
Thread.interrupt()
方法,其函数申明为:public void interrupt();
该方法会将线程的中断标志位置位。
java.lang.Thread
类还两个方法来检查中断标志:
public static boolean interrupted()
{
return currentThread().isInterrupted(true);
}
public boolean isInterrupted()
{
return isInterrupted(false);
}
这两个方法分别调用同一个原生方法来检查中断状态并有所不同。
一方面interrupted()
是静态方法,可以使用Thread.interrupted()
的方法来调用。
而isInterrupted()
则需要一个实例对象作为隐式参数,使用threadInstance.isInterrupted()
来调用。
另一方面,interrupted在调用后会立即将线程中断标志位复位,而isInterrupted则不会这么做。
非阻塞线程的中断问题:
调用Thread.interrupt将目标线程中断标志位置位,然后在目标线程中检查中断标志位。
如果检查到中断之后使用throw new InterruptedException()抛出一个异常,然后使用catch语句来捕获这个异常,则可以在这里完成一些中断逻辑。
特别的,可以使用这种方法退出线程。下面是这种退出方式的举例。
package com.kingbaron.jinterrupt;
/**
* Created by kingbaron on 2016/3/17.
*/
public class UnblockingInterrupt extends Thread{
@Override
public void run(){
super.run();
try{
for(int i=0;i<10000;i++)
{
if(Thread.interrupted())
//if(this.isInterrupted())
{
System.out.println("I'm interrupted and I'm going out");
throw new InterruptedException();
}
System.out.println("i="+(i+1));
}
System.out.println("I'm below the for clauses and I have no chance to run here");
}catch(InterruptedException e){
System.out.println("I'm in the catch clauses of the method UnblockingInterrupt.run()");
e.printStackTrace();
}
}
}
package com.kingbaron.jinterrupt;
/**
* Created by kingbaron on 2016/3/17.
*/
public class TestUnblockingInterrupt {
public static void main(String[] args){
try{
UnblockingInterrupt thread=new UnblockingInterrupt();
thread.start();
Thread.sleep(2000);
thread.interrupt();
}catch(InterruptedException e)
{
System.out.println("main catch");
e.printStackTrace();
}
}
}
输出结果(关键部分)为:
i=277919
i=277920
I'm interrupted and I'm going out
I'm in the catch clauses of the method UnblockingInterrupt.run
java.lang.InterruptedException
at com.kingbaron.jinterrupt.UnblockingInterrupt.run(UnblockingInterrupt.java:16)
可以看到,当线程thread的中断标志位被main线程中使用thread.interrupt置位后,在执行thread线程的for循环的if条件检查时,
Thread.interrupted()返回true随后在其中抛出一个InterruptedException异常,再使用catch子句捕获该异常,由于run方法中在catch子句之后再无语句,
故线程从run方法返回,thread线程就此终止。
阻塞线程的中断问题。
首先,一个线程出于阻塞中断状态(实际上还包括等待状态)的原因,常见的有线程调用了thread.sleep、
thread.join、thread.wait、1.5中的condition.await、以及可中断的通道上的 I/O 操作方法等。
如果一个线程处于阻塞状态,若将其中断标志位置为,则会在产生阻塞的语句出抛出一个InterruptedException异常,
并且在抛出异常后立即将线程的中断标示位复位,即重新设置为false。
抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。
换言之,中断将一个处于阻塞状态的线程的阻塞状态解除。
注意,synchronized在获锁的过程中是不能被中断的,意思是说如果产生了死锁,则不可能被中断(请参考后面的测试例子)。
与synchronized功能相似的reentrantLock.lock()方法也是一样,它也不可中断的。
即如果发生死锁,那么reentrantLock.lock()方法无法终止,如果调用时被阻塞,则它一直阻塞到它获取到锁为止。
但是如果调用带超时的tryLock方法reentrantLock.tryLock(long timeout, TimeUnit unit),那么如果线程在等待时被中断,将抛出一个InterruptedException异常,这是一个非常有用的特性,因为它允许程序打破死锁。
你也可以调用reentrantLock.lockInterruptibly()方法,它就相当于一个超时设为无限的tryLock方法。
没有任何语言方面的需求一个被中断的线程应该终止。中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断。
某些线程非常重要,以至于它们应该不理会中断,而是在处理完抛出的异常之后继续执行,但是更普遍的情况是,一个线程将把中断看作一个终止请求。
这种线程的run方法遵循如下形式:
public void run() {
try {
...
/*
* 不管循环里是否调用过线程阻塞的方法如sleep、join、wait,这里还是需要加上
* !Thread.currentThread().isInterrupted()条件,虽然抛出异常后退出了循环,显
* 得用阻塞的情况下是多余的,但如果调用了阻塞方法但没有阻塞时,这样会更安全、更及时。
*/
while (!Thread.currentThread().isInterrupted()&& more work to do) {
do more work
}
} catch (InterruptedException e) {
//线程在wait或sleep期间被中断了
} finally {
//线程结束前做一些清理工作
}
}
在这里,应当注意一个原则:尽量不要在底层代码中捕获InterruptedException原则。因为该异常一旦被捕获,线程的中断标志位就会被置位。
对于底层代码而言,如果代码会抛出不知道应当如何应对的InterruptException异常,可以有下面两个选择。
(1)捕获到InterruptedException异常后将中断标志位置位,让外层代码根据检查中断标志位来判断是否中断。
public subTask(){
try{
//...the works may throw a InterruptedException
}catch(InterruptedException e){
Thread.currentThread.interrupt();
}
}
(2)更推荐的方法是,底层代码不捕获InterruptedException,直接将其抛给外层代码去解决
public subTask() throws InterruptedException
{
//...
}
中断失效的情况之临界区:
进入临界区的代码是不允许中断的。这一点很好理解,临界区是并行问题为了保护临界资源的互斥访问而特地加锁的,
一旦可以中断,那么锁的存在也就没有任何意义了。特别的,形成了死锁的线程会分别处于阻塞状态,但是它们都无法被中断。
常见的有进入synchronized块的代码以及Lock.lock()之后没能得到锁而处于阻塞的状态。通常可以使用Lock.lockInterruptibly()来代替Lock.lock(),
因为Lock.lockInterruptibly()可以接受中断,这个中断指的是,既然没能得到锁进入临界区,与其阻塞不如做些别的什么事。
一旦进入临界区,任何方法都不得中断。
中断失效的情况之不可中断I/O:
自java 1.4之后对大量数据的I/O常用通道(channels)机制,它是可以被中断的I/O,也就是说如果这种I/O处于阻塞状态,可以使用中断来解除阻塞。
但是存在着一些不可中断的操作,比如ServerSocket.accept(),inputSteam.read()等调用interrupt()对于这几个问题无效,因为它们都不抛出中断异常。如果拿不到资源,它们会无限期阻塞下去。
对于inputStream等资源,有些(实现了interruptibleChannel接口)可以通过close()方法将资源关闭,对应的阻塞也会被放开。
对于处理大型I/O时,推荐使用Channels。