首先要明確的是Java沒有提供任何機制來安全的終止線程。
Java雖然提供了stop方法來終止線程,但是這種方式簡單粗暴,很可能造成數據不一致的情況,因此stop方法已經棄用了。
目前較爲安全地終止線程方式有兩種:
1)使用標誌位,讓線程run方法在合適的時候結束執行,從而終止線程。
2)使用中斷機制,讓線程run方法在中斷的狀態下結束執行,從而終止線程。
ps:這兩種方案雖然都可以安全的終止線程,但是使用標誌位往往具有侷限性,譬如使用阻塞容器的線程,在容器阻塞的情況下,往往沒有機會檢測到到標誌位,從而導致線程無法終止(後面會有實例對比來說明這個問題)。所以,就有下面的結論:
通常,使用中斷機制是實現線程終止的最合理方式。
一、使用標誌位終止線程
如下實例聲明瞭一個volatile類型的boolean變量作爲標誌位,表示線程是否終止,當標誌位爲false時線程才保持運行。
/**
* 通過標誌位來停止線程
*/
static class CancelThreadWorker extends Thread {
private volatile boolean cancelled = false;
public CancelThreadWorker(String name) {
super(name);
}
public void run() {
int i = 0;
while (!cancelled) {
try {
System.out.println(i);
Thread.sleep(2000);
i++;
} catch (InterruptedException e) {
ExceptionUtil.printException(e);
}
}
}
public void cancel() {
cancelled = true;
}
}
ps:爲了後期測試方便,寫個簡單的工具類MyThreadUtil對基礎打印操作做了封裝,如下:
static class MyThreadUtil {
public static void printException(Exception e) {
System.out.println(Thread.currentThread().getName() + " throws an Exception:" + e);
}
public static void printState(Thread thread) {
System.out.println(thread.getName() + " current state is:" + thread.getState());
}
}
main方法測試代碼如下:
CancelThreadWorker cancelThreadWorker = new CancelThreadWorker("cancelThreadWorker");
cancelThreadWorker.start();
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
ExceptionUtil.printException(e);
}
cancelThreadWorker.cancel();
while (cancelThreadWorker.isAlive()) {} //阻塞一小段時間,等待線程終止
System.out.println(cancelThreadWorker.getState());
控制檯輸出信息如下:
0
1
cancelThreadWorker current state is:TERMINATED
可以看出使用標誌位確實讓線程終止了。
二、使用中斷機制
中斷是一種協作機制,調用interrupt方法並不代表線程立刻就停止了,而是告訴線程你應該停止了。
Java中設計一個良好的api通常要考慮對中斷做出及時響應,方便調用者知道被中斷的線程接收到了中斷信息,從而可以提前結束線程。例如Thread.sleep,Object.wait,Thread.join,阻塞容器的take和put,這類方法之所以被設計成拋出中斷異常InterruptedException,就是爲了對中斷做出及時響應,告訴調用者,我已經收到中斷信號了!值得注意的是,這類庫方法在拋出InterruptedException的時候會清除線程的中斷狀態。
如下的實例演示瞭如何通過中斷機制來終止線程,通過判斷當前線程中斷狀態是否爲false,僅當中斷狀態爲false時run方法才執行。
當外部線程調用了該線程的interrupt方法時,sleep方法會對中斷做出響應,拋出InterruptedException,繼而重置中斷狀態爲false。由於run方法限制了異常只能捕獲,所以我們必須對異常及時處理,這裏處理方案有兩種,1)重新調用當前線程的interrupt方法讓中斷狀態恢復爲true。2)直接使用break退出循環即可
static class InterruptThreadWorker extends Thread {
public InterruptThreadWorker(String name) {
super(name);
}
public void run() {
int i = 0;
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println(i);
Thread.sleep(2000);
i++;
} catch (InterruptedException e) {
MyThreadUtil.printException(e);
Thread.currentThread().interrupt(); //由於中斷異常會清除中斷標記,所以此處一定要重新調用中斷方法。當然也可以用break來代替
// break; //使用break代替上面的interrupt也可以
}
}
}
}
main方法測試代碼如下:
InterruptThreadWorker interruptThreadWorker = new InterruptThreadWorker("interruptThreadWorker");
interruptThreadWorker.start();
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
MyThreadUtil.printException(e);
}
interruptThreadWorker.interrupt(); //傳遞中斷信號,告訴interruptThreadWorker,你該終止了哈
while (interruptThreadWorker.isAlive()) {} //阻塞一小段時間,等待線程終止
MyThreadUtil.printState(interruptThreadWorker);
控制檯輸出如下:
0
1
interruptThreadWorker throws an Exception:java.lang.InterruptedException: sleep interrupted
interruptThreadWorker current state is:TERMINATED
可以看出在這個案例和上一個案例的演示結果是一樣的。
三、使用標誌位方案終止線程的侷限性
前面提到過標誌位的終止方案對於阻塞容器來說可能會失效,如下是一個具體的案例,源於<Java併發編程實戰>這本書。
/**
* 素數生產者線程
*/
static class PrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;
private volatile boolean cancelled = false;
public PrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
public void run() {
BigInteger p = BigInteger.ONE.ONE;
while (!cancelled) {
try {
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException e) {
MyThreadUtil.printException(e);
}
}
}
public void cancel() {
cancelled = true;
}
}
素數生產者線程PrimeProducer維護了一個阻塞容器BlockingQueue,run方法使用while循環判斷標誌位cancelled是否爲false,如果滿足則向容器中添加素數。
main方法的測試代碼如下,我們設置阻塞隊列的長度爲9,主線程睡眠20ms之後,設置標誌位爲false。
BlockingQueue<BigInteger> primes = new ArrayBlockingQueue<BigInteger>(9);
PrimeProducer brokenPrimeProducer = new PrimeProducer(primes);
brokenPrimeProducer.start();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
MyThreadUtil.printException(e);
}
brokenPrimeProducer.cancel();
System.out.println(brokenPrimeProducer.queue.size());
while (brokenPrimeProducer.isAlive()) {} //阻塞一小段時間,等待線程終止
MyThreadUtil.printState(brokenPrimeProducer);
控制檯打印如下:
9
細心觀察,發現控制檯其實一直處於運行狀態,並沒有打印這個線程的狀態,也就是說線程並沒有被終止,而是一直處於阻塞狀態。
這是因爲:線程開啓的20ms之後,阻塞隊列已經被填滿,而put作爲一個阻塞方法(類似的take),如果隊列滿則會持續到阻塞到隊列有可用空間爲止。本例中只有生成者線程而沒有消費者線程,導致put方法持續阻塞,所以無法檢測到while循環標誌位的狀態,故線程無法終止。
如果我們將main方法中的阻塞隊列擴大,線程會被正常終止的。只需修改下面一行:
BlockingQueue<BigInteger> primes = new ArrayBlockingQueue<BigInteger>(9999);
或者:你也可以保持隊列容量不變,將主線程的睡眠時間縮小:
Thread.sleep(2);
採用第一種實驗方案,控制檯打印如下:
56
Thread-0 current state is:TERMINATED
可以看出阻塞隊列在添加完第56個元素之後,發現標誌位爲true,不再添加元素,線程終止。證明前面的分析完全成立。
顯然實際項目開發中,我們不可能爲了終止線程來刻意改變阻塞隊列大小,這是一種投機行爲,包含了運氣成分,它可能會影響項目質量。所以對於包含阻塞隊列的線程來說,不提倡使用標誌位的策略終止線程。
本案例中對於素數生產者線程來說,使用中斷是最好的策略,將PrimeProducer改造成以下形式:
/**
* 素數生產者線程
*/
static class PrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;
public PrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
public void run() {
BigInteger p = BigInteger.ONE.ONE;
while (!isInterrupted()) {
try {
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException e) {
MyThreadUtil.printException(e);
cancel();
}
}
}
public void cancel() {
this.interrupt();
}
}
依舊設置阻塞隊列大小爲9,延時20ms,main方法運行效果如下,可以看出控制檯打印之後正常結束,線程不再阻塞。
9
Thread-0 throws an Exception:java.lang.InterruptedException
Thread-0 current state is:TERMINATED
四、stop方法不安全的體現
stop方法不適合用來終止線程,是因爲它是強行結束線程,如果對象是一種不連貫的狀態,即使方法是同步的,也會立刻釋放已持有的鎖,從而造成數據不一致。
如下的案例演示這個問題:
static class UnstableObject {
private String first = "ja";
private String second = "va";
public synchronized void setValues(String first, String second) throws Exception{
this.first = first;
Thread.sleep(10000);
this.second = second;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getSecond() {
return second;
}
public void setSecond(String second) {
this.second = second;
}
}
UntableObject這個類維護了兩個屬性first和second,並提供了一個同步方法setValues對兩個屬性分別賦值,然而賦值過程並不是連貫的,存在一個睡眠10s的過程,如果一個線程調用setValues並在10s內進行被stop,則只會對第一個屬性成功賦值,第二個屬性仍然保持默認。
我們測試代碼如下:
UnstableObject unstableObject = new UnstableObject();
Thread thread = new Thread(()->{
try {
unstableObject.setValues("p", "hp");
} catch (Exception e) {
e.printStackTrace();
}
});
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
MyThreadUtil.printException(e);
}
thread.stop();
MyThreadUtil.printState(thread);
System.out.println(unstableObject.getFirst() + unstableObject.getSecond());
控制檯輸出如下:
Thread-0 current state is:TERMINATED
pva
可以看出線程雖然終止了,但我們渴望輸出php,它卻輸出了pva,這就是stop不安全的一個體現。