多线程设计模式解读—Two-phase Termination,
有时候,我们希望提前结束线程,但安全可靠地停止线程,并不是一件容易的事情,如果立即停止线程,会使共享的数据结构处于不一致的状态,如目前已经废弃使用的Thread类的stop方法(它会使线程在抛出java.lang.ThreadDeath之后终止线程,即使是在执行synchronized方法的时候)。更好的做法是执行完终止处理,再终止线程,即Two-phase Termination,两阶段终止模式。
该模式有两个角色:
Terminator,终止者,负责接收终止请求,执行终止处理,处理完成后再终止自己。
TerminationRequester:终止请求发出者,用来向Terminator发出终止请求。
该模式示例代码如下:
Terminator:
public class TerminateTestThread extends Thread {
// 计算值
private long num = 0;
// 是否关闭标志
private volatile boolean isShutdown = false;
// 终止请求
public void terminate() {
isShutdown = true;
interrupt();
}
// 检查关闭状态
public boolean isShutdown() {
return isShutdown;
}
@Override
public void run() {
try {
while (!isShutdown()) {
doWork();
}
} catch (InterruptedException e) {
} finally {
doShutdown();
}
}
// 操作
private void doWork() throws InterruptedException {
num++;
System.out.println("num: = " + num);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
}
// 终止处理
private void doShutdown() {
System.out.println("doShutdown: Save result");
System.out.println("doShutdown result: num = " + num);
System.out.println("doShutdown: Save END");
}
}
TerminationRequester:
public class TerminationRequesterMain {
public static void main(String[] args) {
try {
// 启动线程
TerminateTestThread t = new TerminateTestThread();
t.start();
//等待一段时间
Thread.sleep(1000);
// 发送线程的终止请求
t.terminate();
// 等待线程终止
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这段代码可以看出实现两阶段终止模式必须注意的是:
使用线程停止标志和interrupt方法,两者缺一不可
public void terminate() {
isShutdown = true;
interrupt();
}
这里使用了isShutdown作为线程停止标志,变量采用volatile修饰,避免了使用显式锁的开销,又保证了内存可见性。线程run方法会检查isShutdown属性,如果属性为true,就停止线程,但线程可能调用了阻塞方法,处于wait状态,任务也就可能永远不会检查isShutdown标志;线程也有可能处于sleep()状态,等sleep时间过后再执行终止状态,程序的响应性就下降了。你可以把方法改成如下运行,线程停止明显变慢了许多:
public void terminate() {
isShutdown = true;
}
因此,需要调用interrupt()方法,发出中断线程的请求。那么,但为什么不直接在run方法中使用isInterrupted方法检查线程是否处于中断状态呢,如下面代码:
@Override
public void run() {
try {
while (!isInterrupted()) {
doWork();
}
} catch (InterruptedException e) {
} finally {
doShutdown();
}
}
运行后发现线程始终没有停止,这是因为在doWork方法内部,sleep方法抛出InterruptedException异常后中断状态被清除,捕获时也没有作出任何处理,需要Thread.currentThread().interrupt()保留线程中断状态,我们把方法稍微改动即可:
// 操作
private void doWork() throws InterruptedException {
num++;
System.out.println("num: = " + num);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
因此,同时使用线程停止标志和interrupt方法,其实是给线程停止操作上了双重保险,开发人员或许会忽略InterruptedExceptio异常,或许线程处于wait或者长时间的sleep的状态,这些情况都要提前考虑好。
以上是一个简单的Two-phase Termination(两阶段终止模式)范例,在复杂实现中,我们可能还要考虑其他方面的内容,如如何停止处于生产者-消费者模式中的线程,停止顺序是怎样的,在停止时如何处理队列中的待处理任务;如果有多个可停止线程,那么线程停止标志怎样实现共享,减少锁的使用。我们需要一套可复用的解决方案,来综合考虑这些问题。
欢迎扫码关注公众号java达人:
drjava