???????對于多線程編程,如何優(yōu)雅的終止子線程,始終是一個值得考究的問題。如果直接終止線程,可能會產(chǎn)生三個問題:
- 子線程當前執(zhí)行的任務可能必須要原子的執(zhí)行,即其要么成功執(zhí)行,要么就不執(zhí)行;
- 當前任務隊列中還有未執(zhí)行完的任務,直接終止線程可能導致這些任務被丟棄;
- 當前線程占用了某些外部資源,比如打開了某個文件,或者使用了某個Socket對象,這些都是無法被垃圾回收的對象,必須由調(diào)用方進行清理。
???????由此可見,如何優(yōu)雅的終止一個線程,并不是一個簡單的問題。常見的終止線程的方式是,聲明一個標志位,如果調(diào)用方要終止其創(chuàng)建的線程的執(zhí)行,就將該標志位設置為需要終止狀態(tài),子線程每次執(zhí)行任務之前會檢查該標志位,如果為需要終止狀態(tài),就不繼續(xù)執(zhí)行任務,而是進行當前線程所占用資源的一些清理工作,如關閉Socket和備份當前未完成的任務,清理完成之后結(jié)束當前線程的調(diào)用。
???????兩階段終止模式使用的就是上述方式進行多線程的終止的,只不過其將線程的終止封裝為一個特定的框架,使用者只需要關注特定的任務執(zhí)行方式即可,從而實現(xiàn)了線程的終止與任務的執(zhí)行的關注點的分離。兩階段終止模式的UML圖如下:
???????其各角色的作用如下:
- ThreadOwner:客戶端程序,由其創(chuàng)建線程并執(zhí)行任務,Terminatable提供的終止方法也是由其調(diào)用;
- Terminatable:終止方法提供的一個抽象接口,提供了一個terminate()方法供外部調(diào)用;
- TerminatableSupport:實現(xiàn)了Terminatable接口的抽象類,封裝了具體的終止模板,其doRun()是一個抽象方法,子類必須實現(xiàn),用于編寫相關的任務的代碼,doTermiate()和doCleanup()方法都是鉤子方法,提供了空的實現(xiàn),子類根據(jù)具體情況判斷是否需要實現(xiàn)該方法;
- ConcreteTerminatable:用戶具體的終止類,其doRun()方法用于實現(xiàn)具體的任務;
- TerminationToken:包含了一個標志位,并且記錄了當前線程還需要執(zhí)行的任務數(shù)量,默認情況下,只有其標志位為true,并且剩余需要執(zhí)行的任務數(shù)為0時才會真正的終止當前線程的執(zhí)行。
???????如下是兩階段終止模式各個類的實現(xiàn),我們首先看看Terminatable接口及其抽象實現(xiàn)TerminatableSupport:
public interface Terminatable {
void terminate();
}
public abstract class TerminatableSupport extends Thread implements Terminatable {
public final TerminationToken terminationToken; // 記錄當前的標志位
public TerminatableSupport() {
this(new TerminationToken()); // 初始化當前標志位
}
public TerminatableSupport(TerminationToken terminationToken) {
super();
this.terminationToken = terminationToken; // 初始化標志位
terminationToken.register(this); // 注冊當前對象的標志位
}
protected abstract void doRun() throws Exception; // 供子類實現(xiàn)具體任務的方法
// 鉤子方法,用于子類進行一些清理工作
protected void doCleanup(Exception cause) {}
// 鉤子方法,用于子類進行終止時的一些定制化操作
protected void doTerminate() {}
@Override
public void run() {
Exception ex = null;
try {
// 在當前線程中執(zhí)行任務時,會判斷是否標識為終止,并且剩余任務數(shù)小于等于0,是才會真正終止當前線程
while (!terminationToken.isToShutdown() || terminationToken.reservations.get() > 0) {
doRun();
}
} catch (Exception e) {
ex = e;
} finally {
try {
doCleanup(ex); // 當前線程終止后需要執(zhí)行的操作
} finally {
terminationToken.notifyThreadTermination(this);
}
}
}
@Override
public void interrupt() {
terminate();
}
@Override
public void terminate() {
terminationToken.setToShutdown(true); // 設置終止狀態(tài)
try {
doTerminate(); // 執(zhí)行客戶端定制的終止操作
} finally {
if (terminationToken.reservations.get() <= 0) {
super.interrupt(); // 如果當前線程處于終止狀態(tài),則強制終止當前線程
}
}
}
// 提供給客戶端調(diào)用的,即客戶端線程必須等待終止完成之后才會繼續(xù)往下執(zhí)行
public void terminate(boolean waitUntilThreadTerminated) {
terminate();
if (waitUntilThreadTerminated) {
try {
this.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
???????當客戶端調(diào)用termiante()方法時,其首先會將當前的終止狀態(tài)設置為true,然后調(diào)用doTerminate()方法,這里需要注意的一點是,如果當前線程在doRun()方法中處于等待狀態(tài),比如Thread.sleep()、Thread.wait()方法等,那么即使設置了終止狀態(tài),也無法使其被喚醒,因為其無法運行到檢測終止狀態(tài)的代碼處,其只能使用intertupt()方法才能使其被喚醒并終止,但是對于Socket.read()方法,即使調(diào)用了interrupt()方法,也無法使其終止,因而這里設置了doTerminate()方法,用于子類在該方法中關閉Socket。最后在finally塊中,調(diào)用super.interrupt()方法,該調(diào)用的作用也即如果當前線程在doRun()方法中被阻塞,就強制終止其執(zhí)行。
public class TerminationToken {
protected volatile boolean toShutdown = false; // 終止狀態(tài)的標志位
public final AtomicInteger reservations = new AtomicInteger(0); // 記錄當前剩余任務數(shù)
// 記錄了所有注冊了TerminationToken的實例,這里使用Queue是因為可能會有多個
// Terminatable實例共享同一個TeraminationToken,如果是共享的,那么reservations
// 實例就保存了所有共享當前TerminationToken實例的線程所需要執(zhí)行的任務總數(shù)
private final Queue<WeakReference<Terminatable>> coordinatedThreads;
public TerminationToken() {
coordinatedThreads = new ConcurrentLinkedQueue<>();
}
public boolean isToShutdown() {
return toShutdown;
}
public void setToShutdown(boolean toShutdown) {
this.toShutdown = toShutdown;
}
// 將當前Terminatable實例注冊到當前TerminationToken中
protected void register(Terminatable thread) {
coordinatedThreads.add(new WeakReference<>(thread));
}
// 如果是多個Terminatable實例注冊到當前TerminationToken中,
// 則廣播當前的終止狀態(tài),使得這些實例都會終止
protected void notifyThreadTermination(Terminatable thread) {
WeakReference<Terminatable> wrThread;
Terminatable otherThread;
while (null != (wrThread = coordinatedThreads.poll())) {
otherThread = wrThread.get();
if (null != otherThread && otherThread != thread) {
otherThread.terminate();
}
}
}
}
???????關于Terminatable和TerminationToken的關系是一對多的關系,即多個Terminatable實例可共用一個TerminationToken實例,而其reservations屬性所保存的則是這多個Terminatable實例所共同要完成的任務數(shù)量。這里典型的多個Terminatable共用一個TerminationToken實例的例子是當有多個工作者線程時,這幾個線程所消費的任務是共用的,因而其TermiantionToken實例也需要共用。
???????兩階段終止模式的使用場景非常的多,基本上只要是使用了子線程的位置都需要使用一定的方式來優(yōu)雅的終止該線程的執(zhí)行。我們這里使用生產(chǎn)者和消費者的例子來演示兩階段終止模式的使用,如下是該例子的代碼:
public class SomeService {
private final BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
private final Producer producer = new Producer();
private final Consumer consumer = new Consumer();
public static void main(String[] args) throws InterruptedException {
SomeService ss = new SomeService();
ss.init();
TimeUnit.SECONDS.sleep(500);
ss.shutdown();
}
// 停止生產(chǎn)者和消費者的執(zhí)行
public void shutdown() {
producer.terminate(true); // 先停止生產(chǎn)者,只有在生產(chǎn)者完全停止之后才會停止消費者
consumer.terminate(); // 停止消費者
}
// 啟動生產(chǎn)者和消費者
public void init() {
producer.start();
consumer.start();
}
// 生產(chǎn)者
private class Producer extends TerminatableSupport {
private int i = 0;
@Override
protected void doRun() throws Exception {
queue.put(String.valueOf(i++)); // 將任務添加到任務隊列中
consumer.terminationToken.reservations.incrementAndGet(); // 更新需要執(zhí)行的任務數(shù)量
}
}
// 消費者
private class Consumer extends TerminatableSupport {
@Override
protected void doRun() throws Exception {
String product = queue.take(); // 獲取任務
System.out.println("Processing product: " + product);
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(100)); // 模擬消費者對任務的執(zhí)行
} catch (InterruptedException e) {
// ignore
} finally {
terminationToken.reservations.decrementAndGet(); // 更新需要執(zhí)行的任務數(shù)量
}
}
}
}
???????可以看到,在子類使用兩階段終止模式時,其只需要實現(xiàn)各自所需要執(zhí)行的任務,并且更新當前任務的數(shù)量即可。在某些情況下,當前任務的數(shù)量也可以不進行更新,比如在進行終止時,不關心當前剩余多少任務需要執(zhí)行。