JAVA并发体系-1.1-终结任务或线程
中断-interrupt
本文讨论的问题是:在某些情况下,任务必须突然的终止。即有时你希望能够终止处于阻塞状态的任务(todo: 关于何时处于堵塞请查看线程和任务中的解释)。如果对于处于阻塞状态的任务,你不能等待其到达代码中可以检查其状态值的某一点,因而决定让它主动地终止,那么你就必须强制这个任务跳出阻塞状态。
在Runnable.run()
方法的中间打断任务,与等待任务执行方法到达检查cancel标志的语句,或者到达程序员准备好离开该执行方法的其他一些地方相比,要棘手得多。
当打断被阻塞的任务时,可能需要清理资源。正因为这一点,在任务的run()方法中间打断,更像是抛出的异常,因此在Java线程中的这种类型的异常中断中用到了异常。虽然,异常从来都不能异步地传递;但是只要在使用对象互斥机制(与 synchronized关键字相对)时使用try- finally惯用法,在指令/方法调用的中间突然中断没有任何危险。如果抛出异常,使用try- finally惯用法这些互斥就会自动被释放。
Thread上的中断
Thread类包含 interrupt()方法,可以使用它终止被阻塞的任务。 interrupt()方法将设置线程的中断状态。
-
调用时刻:如果一个线程已经被阻塞,或者试图执行一个阻塞操作,这时设置这个线程的中断状态将抛出
InterruptedException
- 中断发生的唯一时刻是在任务要进入到阻塞操作中,或者已经在阻塞操作内部时
-
调用过程:当另一个a线程在该b线程上调用 interrupt()方法时,a线程将给该b线程设定一个标志,表明该b线程已经被中断。
-
重置标志位:
InterruptedException
异常被捕获时(即抛出该异常时)将清理interrupt标志,中断状态将被复位。所以在 catch子句中,在异常被捕获的时候interrupt标志总是为假。- 当任务调用
Thread.isInterrupted()
检查中断状态时,中断状态将被复位。 - 如果想要再次检查以了解是否被中断,则可以在调用
Thread.isInterrupted()
时将结果存储起来。
-
调用 interrupt,必须持有 Thread对象。
Executor上的中断操作
新的concurrent类库在避免对Thread对象的直接操作,转而尽量通过 Executor来执行所有操作。但是正如上面所说"调用 interrupt,必须持有 Thread对象。",我们需要知道如何通过 Executor操作。(注意:ExecutorService继承了Executor接口)
-
中断Executor上的所有进程:
在ExecutorService上调用
shutdownNow()
,那么它将发送一个interrupt()
调用给它启动的所有线程。(这么做是有意义的,因为当你完成工程中的某个部分或者整个程序时,通常会希望同时关闭某个特定 Executor的所有任务) -
中断Executor上的单个线程:
通过调用 submit()来启动任务,持有该任务的上下文即Future。持有这种 Future的关键在于你可以在其上调用 cancel(),并因此可以使用它来中断某个特定任务,将true传递给 cancel(),那么它就会拥有在该线程上调用 interrupt以停止这个线程的权限。
ExecutorService exec = Executors.newCachedThreadPool(); Future<?> f = exec.submit(new Runnable(){...}); f.cancel(true); // Interrupts if running
能/不能中断的调用
interrupt()
能够中断的调用:
- 对 sleep()的调用
- 任何要求抛出
InterruptedException
的调用- 例如在
ReentrantLock
上堵塞的任务
- 例如在
- 打断被互斥所堵塞的调用
interrupt()
不能中断的调用:
- 正在试图获取 synchronized锁的任务
- 试图执行IO操作的任务
存在不能中断的调用的事实有点令人烦恼,特别是在创建执行IO的任务时。因为这意味着IO具有锁住你的多线程程序的潜在可能。特别是对于基于Web的程序,这更是关乎利害。
对于这类问题,有一个略显笨拙但是有时确实行之有效的解决方案,即关闭任务在其上发生阻塞的底层资源:
ExecutorService exec = Executors.newCachedThreadPool();
ServerSocket server = new ServerSocket(8080);
exec.execute(new IOBlocked(socketInput));
exec.shutdownNow(); //this will not shutdown
socketInput.close(); // Releases blocked thread and then the thread will shutdown
一个ReentrantLock上中断的例子
在ReentrantLock
上被堵塞的任务被中断的例子如下代码所示,在这个代码中本输出的执行流程为:point 1>> point 2>>point 3>>point 4>>point 5>>point 6>>point 7>>point 8>>point 9
关于这个例子需要注意:尽管不太可能,但是对t.interrupt()
的调用确实可以发生在对blocked.f()
的调用之前,进而产生死锁。
class BlockedMutex {
private Lock lock = new ReentrantLock();
public BlockedMutex() {
// Acquire it right away, to demonstrate interruption
// of a task blocked on a ReentrantLock:
lock.lock();
}
public void f() {
try {
// This will never be available to a second task
lock.lockInterruptibly(); // Special call // point 4
print("lock acquired in f()");
} catch(InterruptedException e) {// point 8
print("Interrupted from lock acquisition in f()");
}
}
}
class Blocked2 implements Runnable {
BlockedMutex blocked = new BlockedMutex();
public void run() {
print("Waiting for f() in BlockedMutex");// point 2
blocked.f();// point 3
print("Broken out of blocked call"); // point 9
}
}
public class Interrupting2 {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new Blocked2());
t.start();// point 1
TimeUnit.SECONDS.sleep(1);// point 5
System.out.println("Issuing t.interrupt()");// point 6
t.interrupt();// point 7
}
}
/*Output:
*Waiting for f() in BlockedMutex
*Issuing t.interrupt()
*Interrupted from lock acquisition in f()
*Broken out of blocked call
*/
中断检查
检查中断,保证总是可以离开任务体或无限循环
如果你只能通过在阻塞调用上抛出异常来退出,那么你就无法总是可以离开run()循环。你可以采取如下两种方法来保证退出任务。
-
可以通过调用
Thread.isInterrupted()
来检查中断状态,这不仅可以告诉你interrupt()
是否被调用过,而且还可以清除中断状态。 -
可以经由单一的
InterruptedException
或单一的成功的Thread.interrupted()
测试来得到这种通知。
虽然中断状态在上面两种方法下会被清除,但正如上文所说的:如果想要再次检查以了解是否被中断,则可以在调用 Thread.isInterrupted()
时将结果存储起来。
中断任务的示范
(策略:通过学习之后的这个代码示例来保证可以退出任务体或无限循环)在下面这个例子中,你可以在不同地点退出 Blocked3.run():在阻塞的 sleep()调用中,或者在非阻塞的数学计算中。你将看到,如果 interrupt(在注释 point2之后(即在非阻塞的操作过程中)被调用,那么首先循环将结束,然后所有的本地对象将被销毁,最后循环会经由 while语句的顶部退出。但是,如果 interrupt()在 point1和 point2之间(在 while语句之后,但是在阻塞操作 sleep()之前或其过程中)被调用,那么这个任务就会在第一次试图调用阻塞操作之前,经由InterruptedException
退出。在这种情况下,在异常被抛出之时唯一被创建出来的 NeedsCleanup
对象将被清除,而你也就有了在catch子句中执行其他任何清除工作的机会。
注意:被设计用来响应 interrupt的类必须建立一种策略,来确保它将保持一致的状态。这通常意味着所有需要清理的对象创建操作的后面,都必须紧跟 try-finally子句,从而使得无论run()循环如何退出,清理都会发生。
class NeedsCleanup {
private final int id;
public NeedsCleanup(int ident) {
id = ident;
print("NeedsCleanup " + id);
}
public void cleanup() {
print("Cleaning up " + id);
}
}
class Blocked3 implements Runnable {
private volatile double d = 0.0;
public void run() {
try {
while(!Thread.interrupted()) {
// point1
NeedsCleanup n1 = new NeedsCleanup(1);
// Start try-finally immediately after definition
// of n1, to guarantee proper cleanup of n1:
try {
print("Sleeping");
TimeUnit.SECONDS.sleep(1);
// point2
NeedsCleanup n2 = new NeedsCleanup(2);
// Guarantee proper cleanup of n2:
try {
print("Calculating");
// A time-consuming, non-blocking operation:
for(int i = 1; i < 2500000; i++)
d = d + (Math.PI + Math.E) / d;
print("Finished time-consuming operation");
} finally {
n2.cleanup();
}
} finally {
n1.cleanup();
}
}
print("Exiting via while() test");
} catch(InterruptedException e) {
print("Exiting via InterruptedException");
}
}
}
public class InterruptingIdiom {
public static void main(String[] args) throws Exception {
if(args.length != 1) {
print("usage: java InterruptingIdiom delay-in-mS");
System.exit(1);
}
Thread t = new Thread(new Blocked3());
t.start();
TimeUnit.MILLISECONDS.sleep(new Integer(args[0]));
t.interrupt();
}
} /* Output: (Sample)
NeedsCleanup 1
Sleeping
NeedsCleanup 2
Calculating
Finished time-consuming operation
Cleaning up 2
Cleaning up 1
NeedsCleanup 1
Sleeping
Cleaning up 1
Exiting via InterruptedException
*///:~