启动一个线程是很容易的,无论是继承Thread类或者实现Runnable方法。大多数时候我们都是希望它能正常运行直到结束,或者自行停止。然而有时候我们希望能够提前结束线程,这个可能是用户取消了某个操作,或者应用程序需要快速关闭。
Java语言本身没有提供任何机制来安全的终结某个线程。但是提供了中断(Interruption),这是一种协作机制,能使一个线程终止另一个线程。这种协作式的机制式式很有必要的,因为我们很少需要一个线程立即停止,这样会造成数据不一致的状态(这就是Thread.stop()为什么被弃用的原因)。当需要停止时,首先清除当前正在执行的工作,然后再结束。
取消一个操作的原因
- 用户请求取消。比如点击 “取消” 按钮
- 有时间限制的操作。比如在有限时间内搜索问题解空间,并得到最优解。当计时器超时的时候,取消正在搜索的任务。
- 应用程序事件。将上述搜索问题解空间的任务分解,当一个任务找到最佳解决方案后,则其他正在执行搜索任务的线程将被取消。
- 错误。比如一个爬虫在搜索网页,并保存到磁盘。当磁盘满时,或者其他错误,所有的搜索任务都会结束。
- 关闭。当一个程序或服务关闭时,对正在执行的任务平缓的关闭(即等待其继续执行完成),或立即关闭(取消任务)。
取消标志
设置一个表示“已请求取消”的标志,然后定期地查看该标志。如果设置了这个标志那么任务将提前结束。下面举个例子:
public class SimpleCancle implements Runnable{
private volatile boolean flag;
public void cancle(){flag = true;}
@Override
public void run() {
int i = 0;
while (!flag) {
System.out.println(i++);
}
System.out.println("run is finished");
}
public static void main(String[] args) {
SimpleCancle sample = new SimpleCancle();
Thread test = new Thread(sample);
test.start();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
sample.cancle();
}
}
}
这里的while循环如果没有设置标志位,那么会一直执行,消耗cpu时钟周期。通过这种设置标志位的行为进行取消可能会存在一定的延迟,就是说在调用cancle方法和run方法执行下一次检查之间可能存在延迟。在finally块中保证一定能取消。
中断
上述使用取消表示确实可以停止任务并退出,虽然可能存在一定的延迟。但是如果while循环中调用了一个阻塞的方法,比如BlockingQueue.put,那么这个任务可能永远不会检查标志,因此永远不会结束。
举个例子:
public class NeverStop implements Runnable{
private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(20);
private volatile boolean flag;
public void run() {
while (!flag) {
try {
queue.put(new Random().nextInt());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void cancle() {flag = true;}
}
那么怎么解决这个问题呢?使用java的中断机制
中断机制
每一个线程都有一个中断状态。当中断一个线程时,这个状态变为true。Thread中包含了中断线程以及查询中断状态的方法,相关的代码如下:
public class Thread {
public void interrupt() {......}
public boolean isInterrupted() {......}
public static boolean interrupted() {......}
}
其中interrupt能中断目标线程,isInterrupted能返回目标线程的中断状态。静态的interrupted将会清除当前线程的中断状态,并返回它之前的指,这也是清除中断状态的唯一方法。
阻塞库方法,例如Thread.sleep,Object.wait,BlockingQueue.put等,都会检查线程何时中断,并且在发现中断时提前返回。在响应中断时的操作包括:清除中断状态,抛出InterruptedException,表示阻塞操作由于中断而提前结束。
因为Thread中的阻塞方法是直接通过native方法实现的。我们这里以阻塞队列的put为例,看看API是怎么封装的,put的阻塞实际是ReentrantLock中的lockInterruptibly()来实现,而lock框架依托于AQS,我们来展示一下:
BlockingQueue:
public void put(E e) throws InterruptedException {
......
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); //可响应中断
......
}
ReentrantLock:
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);//调用了AQS的方法
}
AbstractQueuedSynchronizer:(AQS)
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())//清除中断状态
throw new InterruptedException(); //抛异常
......
}
上面的代码只是方法代码中的一部分,只是想给大家展示一下响应中断的一个处理逻辑。还有一点就是JVM并不能保证阻塞方法检测到中断的速度,但是实际响应速度还是很快。
中断机制的正确理解
上面讲了一个非常棒的取消机制即中断,当需要中断时调用interrupt设置中断状态,如果时非阻塞时,它的中断状态将会被设置,然后线程根据被取消的操作来检查中断状态判断是否发生了中断。那么在这个过程中如果不触发InterruptException,那么中断状态将会一致保持,直到明确地清除中断状态。
public void run() {
try {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("此处中断");
throw new InterruptedException();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("out of while");
}
上面这段代码便没有明确的清除中断状态
需要注意的是,调用interrupt并不意味着立即停止目标线程,只是传递了请求中断的消息,然后由线程在下一个合适的时刻中断自己。
像wait,sleep,join等方法将会严格的处理这个请求,即清除中断状态---->抛出异常。设计良好的代码可以忽略这种请求,只要能使得调用代码对中断请求进行处理即可,比如将这个请求传递给另一个任务;而设计糟糕得代码可能会屏蔽中断请求,从而导致其他的代码也无法对中断请求做出响应。
我们举一个利用中断来取消任务的栗子:
public class CanStop implements Runnable{
private final ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(20);
public void run() {
try {
while (!Thread.currentThread().isInterrupted())
queue.put(new Random().nextInt());
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("out of while");
}
public static void main(String[] args) throws InterruptedException {
NeverStop sample = new NeverStop();
Thread test = new Thread(sample);
test.start();
Thread.sleep(1000);
System.out.println(test.isInterrupted());//false
test.interrupt();
System.out.println(test.isInterrupted());//true
System.out.println(test.isInterrupted());//false
}
}
通常,中断时实现取消的最合理方式。
如何响应中断
由两种方式来处理InterruptException:
- 传递异常(可能在执行某个特定于任务的清除操作之后),从而使你的操作也成为可中断的阻塞方法。
- 恢复中断异常,从而使调用栈的上层代码能够对其进行处理。
传递异常:
private final ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(20);
public Integer getInteger() throws InterruptedException {
return queue.take();
}
如果你不想或者无法传递InterruptedException(比如Runnable形式的任务),那么就需要另一种方式来保存中断请求。一种标准的方法就是通过再次调用interrupt恢复中断状态。不能屏蔽InterruptedException,除非在你代码中实现了线程的中断策略。
恢复中断异常:
对于一些不支持取消但是仍可以调用可中断阻塞方法的操作,需要在循环中调用这些方法,并在发现这些中断后重新尝试。在这种情况下,他们应该在本地保存中断状态,并在返回前恢复状态,而不是在捕获异常时恢复状态。如下列程序所示,阻塞方法put会在入口除检查中断状态,如果已被设置则立即抛出异常,这样可能引起无限循环。代码如下:
public class CanStop{
private final ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(20);
public Integer getInteger() {
boolean interrupt = false;
try{
while(true) {
try{
return queue.take();
}catch (InterruptedException e) {
interrupt = true;
System.out.println("exception occur");
}
}
}finally {
if(interrupt)
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) throws InterruptedException{
CanStop can = new CanStop();
Thread tt = new Thread(() -> {
System.out.println(can.getInteger()+" "+Thread.currentThread().isInterrupted());
});
tt.start();
tt.interrupt();
Thread.sleep(3000);
can.queue.put(23);//加入一个值表示可以继续进行了
}
}
总结
本篇讲了如何去取消一个任务线程
1、取消标志(但如果遇到阻塞情况,无能为力)
2、Java的中断机制,以及响应中断的两种方式。