在Java中无法抢占式地停止一个任务的执行,而是通过中断机制实现了一种协作式的方式来取消任务的执行。外部程序只能向一个线程发送中断请求,然后由任务自己负责在某个合适的时刻结束执行。
1. 设置取消标志
这是最基本也是最简单的停止一个任务执行的办法,即设置一个取消任务执行的标志变量,然后反复检测该标志变量的值。
public class MyTask implements Runnable { private volatile running = true; public void run() { while(running) { //...操作 } } public void stop() { running = false; } }
通常需要使用volatile关键字来修饰标志变量,以保证该任务类是线程安全的。但是,如果run方法中存在阻塞的操作,则该任务可能永远也无法正常退出。
2. 中断线程的执行
每个线程都有一个boolean类型的变量来标志该线程的中断状态,Thread类中包含三个与中断状态相关的方法:
interrupt方法试图中断线程并设置中断状态标志变量为true;
isInterrupted方法测试线程是否已经中断,返回中断状态变量的值;
interrupted方法用于清除线程的中断状态,并返回之前的值,即如果当前线程是中断状态,则重新设置为false,并且返回true;
中断一个线程常用的办法即通过调用interrupt方法试图中断该线程,线程中执行的任务收到中断请求后会选择一个合适的时机结束线程。需要注意的是通常由线程的所有者来从外部中断线程的执行,因为通常只有线程的所有者知道在满足某种条件时可以请求中断线程。
3. 阻塞方法与线程的中断
阻塞方法会使线程进入阻塞的状态,例如:等待获得一个锁、Thread.sleep方法、BlockingQueue的put、take方法等。
大部分的阻塞方法都是响应中断的,即这些方法在线程中执行时如果发现线程被中断,会清除线程的中断状态,并抛出InterruptedException表示该方法的执行过程被从外部中断。响应中断的阻塞方法通常会在入口处先检查线程的中断状态,线程不是中断状态时,才会继续执行。
根据阻塞方法的特性,我们就可以利用中断的机制来结束包含阻塞方法的任务的执行:
public MyTask implements Runnable { public void run() { try { while(!Thread.currentThread().isInterrupted()) { Thread.sleep(3000); //阻塞方法 //...其他操作
return; } } catch(InterruptedException ex) { Thread.currentThread().interrupt(); //恢复中断状态 } } } public class Test { public void method() { Thread thread = new Thread(new MyTask()).start(); //.... thread.interrupt(); //通过中断机制请求结束线程的执行 } }
在上面例子中,在线程的任务中包含了阻塞方法sleep,在线程外部通过interrupt方法请求结束线程的执行,sleep方法在检测到线程处于中断状态时,会清除线程的中断状态并抛出InterruptedException。对于阻塞方法抛出的InterruptedException,通常有两种处理方法:
第一种是重新抛出InterruptedException,将该异常的处理权交给方法调用者,这样该方法也成为了阻塞方法(调用了阻塞方法并且抛出InterruptedException);
第二种是通过interrupt方法恢复线程的中断状态,这样可以使得处理该线程的其他代码能够检测到线程的中断状态;
上面的例子采用的是第二种方法,因为阻塞方法是在Runnable接口的run方法中执行的,并没有其他客户方法直接调用Runnable的run方法,因此没有接收InterruptedException的调用者。
对于不支持取消但仍然调用了响应中断的阻塞方法的任务,应该先在本地保存中断状态,然后在任务结束时恢复中断状态,而不是在捕获InterrruptedException时就恢复中断状态:
public class MyTask implements Runnable { boolean interrupted = false; public void run() { try { while(true) //不支持取消操作 { try { Thread.sleep(3000); //...其他操作
return; } catch(InterruptedException ex) { interrupted = true; //在本地保存中断状态
//Thread.currentThread().interrupt(); //不要在这儿立即恢复中断 } } } finally { if(interrupted) Thread.currentThread().interrupt(); //恢复中断 } } }
在上面的例子中,由于大部分响应中断阻塞方法都会在方法的入口处检查线程的中断状态,如果在捕获InterruptedException的地方立即恢复中断,则可能导致刚恢复的中断状态被阻塞方法的入口处被检测到,从而又再次抛出InterruptedException,这样可能导致程序陷入死循环。
也有一些阻塞方法是不响应中断的,即在收到中断请求时不会抛出InterruptedException,如:java.io包中的Socket I/O方法、java.nio.channels包中的InterruptibleChannel类的相关阻塞方法、java.nio.channels包中的Selector类的select方法等。
如果线程执行的任务中包含这类不响应中断的方法,则无法通过标准的中断机制来结束任务的运行,但仍然有其他办法。如:
对于java.io包的Socket I/O方法,可以通过关闭套接字,从而使得read或者write方法抛出SocketException而跳出阻塞;
java.nio.channels包中的InterruptibleChannel类的方法其实是响应线程的interrupt方法的,只是抛出的不是InterruptedException,而是ClosedByInterruptedException,除此之外,也可以通过调用InterruptibleChannel的close方法来使线程跳出阻塞方法,并抛出AsynchronousClosedException;
对于java.nio.channels包的Selector类的select方法,可以通过调用Selector类的close方法或者wakeup方法从而抛出ClosedSelectorExeception;
可以通过改写Thread类的interrupt方法从而将非标准的中断线程的机制封装在Thread中,以中断包含Socket I/O的任务为例:
public class ReadThread extends Thread { private final Socket client; private final InputStream in; public ReadThread(Socket client) throws IOException { this.client = client; in = client.getInputStream(); } public void interrupt() { try { socket.close(); } catch(IOException ignore){} finally { super.interrupt(); } } public void run() { //调用in.read方法 } }
4. 通过Future来取消任务的执行
Future接口有一个cancel方法,可以通过该方法取消任务的执行,cancel方法有一个boolean型的参数mayInterruptIfRunning。
如果设置为false,对于正在执行的任务只能等到任务执行完毕,无法中断;
如果设置为true,对于正在执行的任务可以试图中断任务的运行,这种情况通常只在与Executor框架配合时使用,因为执行任务的线程是由Executor创建的,Executor知道该如何中断执行任务的线程;
puhblic class Test { private Executor executor = Executors.newSingleThreadExecutor(); public static void timedRun(Runnable runnable,long timeout,TimeUnit unit) throws InterruptedException { try { Future<?> task = executor.submit(runnable); task.get(timeout,unit); //任务最多运行指定的时间 } catch(TimeoutException e1){} catch(ExecutionException e2) { throw e2.getCause(); } finally { task.cancel(true); //取消任务的执行 } } }
参考资料 《Java并发编程实战》