如果外部代码能在某个操作正常完成之前将其置入“完成”状态,那么这个操作就可以称为可取消的(Cancellable)。取消某个操作的原因很多:
用户请求取消。用户点击图形界面程序中的“取消”按钮,或者通过管理接口来发出取消请求,例如JMX (Java Management Extensions)。
有时间限制的操作。例如,某个应用程序需要在有限时间内搜索问题空间,并在这个时间内选择最佳的解决方案。当计时器超时时,需要取消所有正在搜索的任务。
应用程序事件。例如,应用程序对某个问题空间进行分解并搜索,从而使不同的任务可以搜索问题空间中的不同区域。当其中一个任务找到了解决方案时,所有其他仍在搜索的任务都
虽然Thread. stop和suspend等方法提供了这样的机制,但由于存在着一些严重的缺陷,因此应该避免使用。请参见http://java.sun.com/j2se/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html了解对这些问题的详细说明。
将被取消。
错误。网页爬虫程序搜索相关的页面,并将页面或摘要数据保存到硬盘。当一个爬虫任务发生错误时(例如,磁盘空间已满),那么所有搜索任务都会取消,此时可能会记录它们的当前状态,以便稍后重新启动。
关闭。当一个程序或服务关闭时,必须对正在处理和等待处理的工作执行某种操作。在平缓的关闭过程中,当前正在执行的任务将继续执行直到完成,而在立即关闭过程中,当前的任务则可能取消。
在Java 中没有一种安全的抢占式方法来停止线程,因此也就没有安全的抢占式方法来停止任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议。
其中一种协作机制能设置某个“已请求取消(Cancellation Requested)”标志,而任务将定期地查看该标志。如果设置了这个标志,那么任务将提前结束。程序清单7-1中就使用了这项技术,其中的PrimeGenerator 持续地枚举素数,直到它被取消。cancel方法将设置cancelled标志,并且主循环在搜索下一个素数之前会首先检查这个标志。(为了使这个过程能可靠地工作,标志cancelled必须为volatile 类型。)
public class PrimeGenerator implements Runnable {
@GuardedBy("this")
private final List
=new ArrayList
private volatile boolean cancelled;
public void run(){
BigInteger p =BigInteger. ONE;
while (Icancelled ){
p =p. nextProbablePrime();
synchronized (this){
primes. add(p);
}
}
}
public void cancel(){cancelled =true;}
public synchronized List
return new ArrayList
}
}
程序清单7-2 给出了这个类的使用示例,即让素数生成器运行1秒钟后取消。素数生成器通常并不会刚好在运行1秒钟后停止,因为在请求取消的时刻和run方法中循环执行下一次检查之间可能存在延迟。cancel方法由finally 块调用,从而确保即使在调用sleep 时被中断也能取消素数生成器的执行。如果cancel 没有被调用,那么搜索素数的线程将永远运行下去,不断消耗CPU的时钟周期,并使得JVM 不能正常退出。
程序清单7-2 一个仅运行一秒钟的素数生成器
List
PrimeGenerator generator =new PrimeGenerator();
new Thread (generator). start();
try {
SECONDS. sleep(1);
}finally {
generator. cancel();
}
return generator. get();
}
一个可取消的任务必须拥有取消策略(Cancellation Policy),在这个策略中将详细地定义取消操作的“How”、“When”以及“What”,即其他代码如何(How)请求取消该任务,任务在何时(When)检查是否已经请求了取消,以及在响应取消请求时应该执行哪些(What)操作。
考虑现实世界中停止支付(Stop-Payment)支票的示例。银行通常都会规定如何提交一个停止支付的请求,在处理这些请求时需要做出哪些响应性保证,以及当支付中断后需要遵守哪些流程(例如通知该事务中涉及的其他银行,以及对付款人的账户进行费用评估)。这些流程和保证放在一起就构成了支票支付的取消策略。
PrimeGenerator 使用了一种简单的取消策略:客户代码通过调用cancel来请求取消,PrimeGenerator在每次搜索素数前首先检查是否存在取消请求,如果存在则退出。
中断
PrimeGenerator中的取消机制最终会使得搜索素数的任务退出,但在退出过程中需要花费一定的时间。然而,如果使用这种方法的任务调用了一个阻塞方法,例如BlockingQueue. put,那么可能会产生一个更严重的问题——任务可能永远不会检查取消标志,因此永远不会结束。
在程序清单7-3 中的BrokenPrimeProducer就说明了这个问题。生产者线程生成素数,并将它们放入一个阻塞队列。如果生产者的速度超过了消费者的处理速度,队列将被填满, put 方法也会阻塞。当生产者在put方法中阻塞时,如果消费者希望取消生产者任务,那么将发生什么情况?它可以调用cancel方法来设置cancelled标志,但此时生产者却永远不能检查这个标志,因为它无法从阻塞的put 方法中恢复过来(因为消费者此时已经停止从队列中取出素数,所以put 方法将一直保持阻塞状态)。
th'is. queue =queue;
}
public void run(){
try {
BigInteger p =BigInteger. ONE;
while (Icancelled)
queue. put(p=p. nextProbablePrime());
}catch (InterruptedException consumed){}
}
public void cancel(){cancelled =true;}
}
void consumePrimes() throws InterruptedException {
BlockingQueue
BrokenPrimeProducer producer =new BrokenPrimeProducer(primes);
producer. start();
try {
while (needMorePrimes())
consume(primes. take());
}finally {
producer. cancel();
}
}
一些特殊的阻塞库的方法支持中断。线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适的或者可能的情况下停止当前工作,并转而执行其他的工作。
在Java的API或语言规范中,并没有将中断与任何取消语义关联起来,但实际上,如果在取消之外的其他操作中使用中断,那么都是不合适的,并且很难支撑起更大的应用。
每个线程都有一个boolean类型的中断状态。当中断线程时,这个线程的中断状态将被设置为true。在Thread 中包含了中断线程以及查询线程中断状态的方法,如程序清单7-4所示。interrupt方法能中断目标线程,而isInterrupted 方法能返回目标线程的中断状态。静态的interrupted 方法将清除当前线程的中断状态,并返回它之前的值,这也是清除中断状态的唯一方法。
程序清单7-4 Thread 中的中断方法
public class Thread {
public void interrupt(){…}
public boolean isInterrupted(){……}
public static boolean interrupted(){…}
...
, ,
阻塞库方法,例如Thread. sleep 和Object. wait等,都会检查线程何时中断,并且在发现中断时提前返回。它们在响应中断时执行的操作包括:清除中断状态,抛出InterruptedException,表示阻塞操作由于中断而提前结束。JVM 并不能保证阻塞方法检测到中断的速度,但在实际情况中响应速度还是非常快的。
当线程在非阻塞状态下中断时,它的中断状态将被设置,然后根据将被取消的操作来检查中断状态以判断发生了中断。通过这样的方法,中断操作将变得“有黏性”——如果不触发InterruptedException,那么中断状态将一直保持,直到明确地清除中断状态。
调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。
对中断操作的正确理解是:它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。(这些时刻也被称为取消点)。有些方法,例如wait、sleep 和join等,将严格地处理这种请求,当它们收到中断请求或者在开始执行时发现某个已被设置好的中断状态时,将抛出一个异常。设计良好的方法可以完全忽略这种请求,只要它们能使调用代码对中断请求进行某种处理。设计糟糕的方法可能会屏蔽中断请求,从而导致调用栈中的其他代码无法对中断请求作出响应。
在使用静态的interrupted 时应该小心,因为它会清除当前线程的中断状态。如果在调用interrupted 时返回了true,那么除非你想屏蔽这个中断,否则必须对它进行处理——可以抛出InterruptedException,或者通过再次调用interrupt来恢复中断状态,如程序清单5-10 所示。
BrokenPrimeProducer 说明了一些自定义的取消机制无法与可阻塞的库函数实现良好交互的原因。如果任务代码能够响应中断,那么可以使用中断作为取消机制,并且利用许多库类中提供的中断支持。
通常,中断是实现取消的最合理方式。
BrokenPrimeProducer 中的问题很容易解决(和简化):使用中断而不是boolean标志来请求取消,如程序清单7-5所示。在每次迭代循环中,有两个位置可以检测出中断:在阻塞的put 方法调用中,以及在循环开始处查询中断状态时。由于调用了阻塞的put 方法,因此这里并不一定需要进行显式的检测,但执行检测却会使PrimeProducer 对中断具有更高的响应性,因为它是在启动寻找素数任务之前检查中断的,而不是在任务完成之后。如果可中断的阻塞方法的调用频率并不高,不足以获得足够的响应性,那么显式地检测中断状态能起到一定的帮助作用。
程序清单7-5 通过中断来取消
class PrimeProducer extends Thread {
private final BlockingQueue
PrimeProducer(BlockingQueue
this. queue =queue;
}
public void run(){
try {
BigInteger p =BigInteger. ONE;
while (!Thread. currentThread(). isInterrupted())
queue. put (p =p. nextProbablePrime());
}catch (InterruptedException consumed){
/* 允许线程退出 */
}
}
public void cancel(){interrupt();}
}
中断策略
'正如任务中应该包含取消策略一样,线程同样应该包含中断策略。中断策略规定线程如何解释某个中断请求——当发现中断请求时,应该做哪些工作(如果需要的话),哪些工作单元对于中断来说是原子操作,以及以多快的速度来响应中断。
最合理的中断策略是某种形式的线程级(Thread-Level)取消操作或服务级(Service-Level)取消操作:尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。此外还可以建立其他的中断策略,例如暂停服务或重新开始服务,但对于那些包含非标准中断策略的线程或线程池,只能用于能知道这些策略的任务中。
区分任务和线程对中断的反应是很重要的。一个中断请求可以有一个或多个接收者——中断线程池中的某个工作者线程,同时意味着“取消当前任务”和“关闭工作者线程”。
任务不会在其自己拥有的线程中执行,而是在某个服务(例如线程池)拥有的线程中执行。对于非线程所有者的代码来说(例如,对于线程池而言,任何在线程池实现以外的代码),应该小心地保存中断状态,这样拥有线程的代码才能对中断做出响应,即使“非所有者”代码也可以做出响应。(当你为一户人家打扫房屋时,即使主人不在,也不应该把在这段时间内收到的邮件扔掉,而应该把邮件收起来,等主人回来以后再交给他们处理,尽管你可以阅读他们的杂志。)
这就是为什么大多数可阻塞的库函数都只是抛出InterruptedException 作为中断响应。它们永远不会在某个由自己拥有的线程中运行,因此它们为任务或库代码实现了最合理的取消策略:尽快退出执行流程,并把中断信息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作。
当检查到中断请求时,任务并不需要放弃所有的操作——它可以推迟处理中断请求,并直到某个更合适的时刻。因此需要记住中断请求,并在完成当前任务后抛出InterruptedException或者表示已收到中断请求。这项技术能够确保在更新过程中发生中断时,数据结构不会被破坏。
任务不应该对执行该任务的线程的中断策略做出任何假设,除非该任务被专门设计为在服务中运行,并且在这些服务中包含特定的中断策略。无论任务把中断视为取消,还是其他某个中断响应操作,都应该小心地保存执行线程的中断状态。如果除了将InterruptedException 传递给调用者外还需要执行其他操作,那么应该在捕获InterruptedException之后恢复中断状态:
Thread. currentThread(). interrupt();
正如任务代码不应该对其执行所在的线程的中断策略做出假设,执行取消操作的代码也不应该对线程的中断策略做出假设。线程应该只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的取消机制中,例如关闭(shutdown)方法。
由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。
批评者曾嘲笑Java 的中断功能,因为它没有提供抢占式中断机制,而且还强迫开发人员必须处理InterruptedException。然而,通过推迟中断请求的处理,开发人员能制定更灵活的中断策略,从而使应用程序在响应性和健壮性之间实现合理的平衡。
响应中断
在5.4节中,当调用可中断的阻塞函数时,例如Thread. sleep 或BlockingQueue. put等,有两种实用策略可用于处理InterruptedException:
·传递异常(可能在执行某个特定于任务的清除操作之后),从而使你的方法也成为可中断的阻塞方法。
·恢复中断状态,从而使调用栈中的上层代码能够对其进行处理。
传递InterruptedException 与将InterruptedException添加到throws子句中一样容易,如程序清单7-6中的getNextTask所示。
程序清单7-6将InterruptedException传递给调用者
BlockingQueue
public Task getNextTask() throws InterruptedException {`
如果不想或无法传递InterruptedException(或许通过Runnable 来定义任务),那么需要寻找另一种方式来保存中断请求。一种标准的方法就是通过再次调用interrupt来恢复中断状态。你不能屏蔽InterruptedException,例如在catch 块中捕获到异常却不做任何处理,除非在你的代码中实现了线程的中断策略。虽然PrimeProducer 屏蔽了中断,但这是因为它已经知道线程将要结束,因此在调用栈中已经没有上层代码需要知道中断信息。由于大多数代码并不知道它们将在哪个线程中运行,因此应该保存中断状态。
只有实现了线程中断美味的代码才可以拼成中断铸装。在常规的任务和群代码中标
不应该屏蔽中断请求
对于一些不支持取消但仍可以调用可中断阻塞方法的操作,它们必须在循环中调用这些方法,并在发现中断后重新尝试。在这种情况下,它们应该在本地保存中断状态,并在返回前恢复状态而不是在捕获InterruptedException时恢复状态,如程序清单7-7所示。如果过早地设置中断状态,就可能引起无限循环,因为大多数可中断的阻塞方法都会在入口处检查中断状态,并且当发现该状态已被设置时会立即抛出InterruptedException。(通常,可中断的方法会在阻塞或进行重要的工作前首先检查中断,从而尽快地响应中断)。
public Task getNextTask(BlockingQueue
boolean interrupted =false;
try {
while (true){
try {
return queue. take();
}catch (InterruptedException e){
interrupted =true;
//重新尝试
}
}
}finally {
if (interrupted)
Thread. currentThread(). interrupt();
}
}
如果代码不会调用可中断的阻塞方法,那么仍然可以通过在任务代码中轮询当前线程的中断状态来响应中断。要选择合适的轮询频率,就需要在效率和响应性之间进行权衡。如果响应性要求较高,那么不应该调用那些执行时间较长并且不响应中断的方法,从而对可调用的库代码进行一些限制。
在取消过程中可能涉及除了中断状态之外的其他状态。中断可以用来获得线程的注意,并且由中断线程保存的信息,可以为中断的线程提供进一步的指示。(当访问这些信息时,要确保使用同步。)
例如,当一个由ThreadPoolExecutor 拥有的工作者线程检测到中断时,它会检查线程池是否正在关闭。如果是,它会在结束之前执行一些线程池清理工作,否则它可能创建一个新线程将线程池恢复到合理的规模。
示例:计时运行
许多问题永远也无法解决(例如,枚举所有的素数),而某些问题,能很快得到答案,也可能永远得不到答案。在这些情况下,如果能够指定“最多花10分钟搜索答案”或者“枚举出在10分钟内能找到的答案”,那么将是非常有用的。
程序清单7-2 中的aSecondOfPrimes方法将启动一个PrimeGenerator,并在1秒钟后中断。尽管PrimeGenerator可能需要超过1秒的时间才能停止,但它最终会发现中断,然后停止,并使线程结束。在执行任务时的另一个方面是,你希望知道在任务执行过程中是否会抛出异常。
如果PrimeGenerator 在指定时限内抛出了一个未检查的异常,那么这个异常可能会被忽略,因为素数生成器在另一个独立的线程中运行,而这个线程并不会显式地处理异常。
在程序清单7-8 中给出了在指定时间内运行一个任意的Runnable的示例。它在调用线程中运行任务,并安排了一个取消任务,在运行指定的时间间隔后中断它。这解决了从任务中抛出未检查异常的问题,因为该异常会被timedRun的调用者捕获。
cancelExec. schedule(new Runnable(){
public void run(){taskThread. interrupt();}
}, timeout, unit);
r. run ( ) ;
}
这是一种非常简单的方法,但却破坏了以下规则:在中断线程之前,应该了解它的中断策略。由于timedRun可以从任意一个线程中调用,因此它无法知道这个调用线程的中断策略。如果任务在超时之前完成,那么中断timedRun所在线程的取消任务将在timedRun返回到调用者之后启动。我们不知道在这种情况下将运行什么代码,但结果一定是不好的。(可以使用schedule返回的ScheduledFuture 来取消这个取消任务以避免这种风险,这种做法虽然可行,但却非常复杂。)
而且,如果任务不响应中断,那么timedRun会在任务结束时才返回,此时可能已经超过了指定的时限(或者还没有超过时限)。如果某个限时运行的服务没有在指定的时间内返回,那么将对调用者带来负面影响。
在程序清单7-9 中解决了aSecondOfPrimes的异常处理问题以及之前解决方案中的问题。执行任务的线程拥有自己的执行策略,即使任务不响应中断,限时运行的方法仍能返回到它的调用者。在启动任务线程之后,timedRun将执行一个限时的join 方法。在join 返回后,它将检查任务中是否有异常抛出,如果有的话,则会在调用timedRun的线程中再次抛出该异常。由于Throwable将在两个线程之间共享,因此该变量被声明为volatile类型,从而确保安全地将其从任务线程发布到timedRun线程。
public void run(){
try { r. run() ;}
catch (Throwable t){this. t =t;}
}
void rethrow(){
if ( t != null)
throw launderThrowable(t);
}
}
RethrowableTask task =new RethrowableTask();
final Thread taskThread =new Thread (task);
taskThread. start();
cancelExec. schedule(new Runnable(){
public void run(){taskThread. interrupt();}
}, timeout, unit);
taskThread. join(unit. toMillis(timeout));
task. rethrow();
}
在这个示例的代码中解决了前面示例中的问题,但由于它依赖于一个限时的join,因此存在着join的不足:无法知道执行控制是因为线程正常退出而返回还是因为join 超时而返回。
通过Future 来实现取消
我们已经使用了一种抽象机制来管理任务的生命周期,处理异常,以及实现取消,即Future。通常,使用现有库中的类比自行编写更好,因此我们将继续使用Future和任务执行框架来构建timedRun。
ExecutorService. submit将返回一个Future 来描述任务。Future拥有一个cancel 方法,该方法带有一个boolean 类型的参数mayInterruptIfRunning,表示取消操作是否成功。(这只是表示任务是否能够接收中断,而不是表示任务是否能检测并处理中断。)如果mayInterruptIfRunning 为true 并且任务当前正在某个线程中运行,那么这个线程能被中断。如果这个参数为false,那么意味着“若任务还没有启动,就不要运行它”,这种方式应该用于那些不处理中断的任务中。
除非你清楚线程的中断策略,否则不要中断线程,那么在什么情况下调用cancel可以将参数指定为true?执行任务的线程是由标准的Executor 创建的,它实现了一种中断策略使得任务可以通过中断被取消,所以如果任务在标准Executor中运行,并通过它们的Future 来取消任务,那么可以设置mayInterruptIfRunning。当尝试取消某个任务时,不宜直接中断线程池,因为你并不知道当中断请求到达时正在运行什么任务——只能通过任务的Future 来实现取消。这也是在编写任务时要将中断视为一个取消请求的另一个理由:可以通过任务的Future来取消它们。
程序清单7-10 给出了另一个版本的timedRun:将任务提交给一个ExecutorService,并通过一个定时的Future. get 来获得结果。如果get 在返回时抛出了一个TimeoutException,那么任务将通过它的Future来取消。(为了简化代码,这个版本的timedRun在finally 块中将直接调用Future. cancel,因为取消一个已完成的任务不会带来任何影响。)如果任务在被取消前就抛出一个异常,那么该异常将被重新抛出以便由调用者来处理异常。在程序清单7-10中还给出了另一种良好的编程习惯:取消那些不再需要结果的任务。(在程序清单6-13和程序清单6-16 中使用了相同的技术。)
程序清单7-10通过 Future 来取消任务
public static void timedRun(Runnable r,
long timeout,TimeUnit unit)
throws InterruptedException {
Future>task =taskExec. submit(r);
try {
task. get(timeout, unit);
}catch (Timeout Exception e){
// 接下来任务将被取消
}catch (ExecutionException e){
∥如果在任务中抛出了异常,那么重新抛出该异常
throw launderThrowable(e. getCause());
}finally {
//如果任务已经结束,那么执行取消操作也不会带来任何影响
task. cancel(true);//如果任务正在运行,那么将被中断
}
}
当Future. get 抛出InterruptedException或TimeoutException时,如果你知道不再需要结果,那么就可以调用Future. cancel来取消任务。
处理不可中断的阻塞
在Java 库中,许多可阻塞的方法都是通过提前返回或者抛出InterruptedException来响应中断请求的,从而使开发人员更容易构建出能响应取消请求的任务。然而,并非所有的可阻塞方法或者阻塞机制都能响应中断;如果一个线程由于执行同步的Socket I/O.或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外没有其他任何作用。对于那些由于执行不可中断操作而被阻塞的线程,可以使用类似于中断的手段来停止这些线程,但这要求我们必须知道线程阻塞的原因。
Java. io 包中的同步Socket I/O。在服务器应用程序中,最常见的阻塞. I/O形式就是对套接字进行读取和写入。虽然InputStream和OutputStream中的read 和write 等方法都不会响应中断,但通过关闭底层的套接字,可以使得由于执行read 或write等方法而被阻塞的线程抛出一个SocketException。
Java. io包中的同步I/O。当中断一个正在InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptException 并关闭链路(这还会使得其他在这条链路上阻塞的线程同样抛出ClosedByInterruptException)。当关闭一个InterruptibleChannel时,将导致所有在链路操作上阻塞的线程都抛出AsynchronousCloseException。大多数标准的Channel 都实现了InterruptibleChannel。
Selector 的异步I/O。如果一个线程在调用Selector. select 方法(在java. nio. channels中)时阻塞了,那么调用close 或wakeup 方法会使线程抛出ClosedSelectorException 并提前返回。
获取某个锁。如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁,所以将不会理会中断请求。但是,在Lock 类中提供了lockInterruptibly 方法,该方法允许在等待一个锁的同时仍能响应中断,请参见第13章。
程序清单7-11的ReaderThread 给出了如何封装非标准的取消操作。ReaderThread管理了一个套接字连接,它采用同步方式从该套接字中读取数据,并将接收到的数据传递给processBuffer。为了结束某个用户的连接或者关闭服务器,ReaderThread 改写了interrupt方法,使其既能处理标准的中断,也能关闭底层的套接字。因此,无论ReaderThread 线程是在read 方法中阻塞还是在某个可中断的阻塞方法中阻塞,都可以被中断并停止执行当前的工作。
public class ReaderThread extends Thread {
private final Socket socket;
private final InputStream in;
public ReaderThread(Socket socket) throws IOException {
this. socket =socket;
this. in =socket. get InputStream();
}
public void interrupt(){
try {
socket. close();
}
catch (IOException ignored){}
finally {
super. interrupt();
}
}
public void run().{
try {
byte[]buff=new byte[BUFSZ];
while (true){
int count =in. read(buff);
if (count < 0)
break;
else if (count >0)
processBuffer(buff, count);
}
} catch (IOException e) {/*允许线程退出 */ }
}
}
采用newTaskFor来封装非标准的取消
我们可以通过newTaskFor方法来进一步优化ReaderThread中封装非标准取消的技术,这
是Java 6在ThreadPoolExecutor中的新增功能。当把一个Callable 提交给ExecutorService时,submit 方法会返回一个Future,我们可以通过这个Future 来取消任务。newTaskFor是一个工厂方法,它将创建Future来代表任务。newTaskFor 还能返回一个RunnableFuture 接口,该接口扩展了Future和Runnable(并由FutureTask实现)。
通过定制表示任务的Future可以改变Future. cancel的行为。例如,定制的取消代码可以实现日志记录或者收集取消操作的统计信息,以及取消一些不响应中断的操作。通过改写interrupt方法,ReaderThread 可以取消基于套接字的线程。同样,通过改写任务的Future.cancel方法也可以实现类似的功能。
在程序清单7-12 的CancellableTask中定义了一个CancellableTask接口,该接口扩展了Callable,并增加了一个cancel 方法和一个newTask工厂方法来构造RunnableFuture。CancellingExecutor 扩展了ThreadPoolExecutor,并通过改写newTaskFor 使得CancellableTask可以创建自己的Future。
public interface CancellableTask
void cancel();
RunnableFuture
}
@ThreadSafe
public class CancellingExecutor extends ThreadPoolExecutor {
...
protected
if (callable instanceof CancellableTask)
return ((CancellableTask
else
return super. newTaskFor(callable);
}
}
public abstract class SocketUsingTask
implements CancellableTask
@GuardedBy("this") private socket socket;
protected synchronized void setSocket(Socket s){socket =s;}
public synchronized void cancel(){
try
if (socket !=null)
socket. close();
}catch (IOException ignored){}
}
public RunnableFuture
return new FutureTask
public boolean cancel(boolean mayInterruptIfRunning){
try {
SocketUsingTask. this. cancel();
}finally {。
return super. cancel(mayInterruptIfRunning);
1
SocketUsingTask实现了CancellableTask,并定义了Future. cancel来关闭套接字和调用super. cancel。如果SocketUsingTask 通过其自己的Future来取消,那么底层的套接字将被关闭并且线程将被中断。因此它提高了任务对取消操作的响应性:不仅能够在调用可中断方法的同时确保响应取消操作,而且还能调用可阻调的套接字I/O 方法。