背景
在開發(fā)某個(gè)組件時(shí),需要定期從數(shù)據(jù)庫中拉取數(shù)據(jù)。由于整個(gè)邏輯非常簡單,因此就啟用了一個(gè)子線程(Thread)使用while循環(huán)+線程休眠來定期更新。
這時(shí)候我又想起一個(gè)老生常談的問題——如何優(yōu)雅地停止線程?
思路
大家都知道,Thread的stop方法早已廢除,在高速上一腳猛剎,很可能人仰馬翻,太危險(xiǎn)。
時(shí)至今日,這個(gè)問題早已有常規(guī)解決方案,即檢測線程的interrupt變量值對應(yīng)中斷狀態(tài)(下簡稱interrupt狀態(tài))時(shí)停止循環(huán),也就是類似如下的形式:
while(!中斷狀態(tài)) { // interrupt狀態(tài)
// do sth...
}
這個(gè)方案的確非常常規(guī),但每次到用的時(shí)候總會(huì)憂心忡忡——要知道跟線程interrupt狀態(tài)相關(guān)的方法可是有多種,他們有什么區(qū)別?這樣做能保證正常中斷嗎?Java進(jìn)程運(yùn)行結(jié)束的時(shí)候這個(gè)線程會(huì)終止嗎(涉及到Tomcat的重啟問題)?
中斷相關(guān)的方法
Thread.interrupted()
實(shí)際上調(diào)用的是Thread.currentThread().isInterrupted(true)。
Thread.currentThread().isInterrupted()
實(shí)際上調(diào)用的是Thread.currentThread()這個(gè)對象的isInterrupted()。
thread.isInterrupted()
實(shí)際上調(diào)用的是thread.isInterrupted(false)。
Thread.currentThread().interrupt()
同樣的,調(diào)用的是Thread.currentThread()這個(gè)對象的interrupt()方法。
thread.interrupt()
代碼如下:
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
分析
本質(zhì)上來說,在Thread類中線程中斷相關(guān)的方法有3個(gè):
- Thread.interrupted()
- thread.isInterrupted()
- thread.interrupt()
盡管Thread.interrupted()和thread.isInterrupted()最終都是調(diào)用Thread對象也就是thread的isInterrupted方法,只是參數(shù)不同,但thread.isInterrupted(boolean)方法是個(gè)私有方法,即調(diào)用該方法只能通過Thread.interrupted()或thread.isInterrupted()。
沒轍,還不如干脆就當(dāng)做不同的方法來看。
ps. 顯然,Thread.currentThread()對象肯定是目前在執(zhí)行這個(gè)循環(huán)的線程對象,也就是誰調(diào)的這個(gè)方法所屬的線程,包含主線程。
thread.isInterrupted(boolean ClearInterrupted)
這是一個(gè)native方法,注釋中寫道:
檢測某個(gè)線程是否被中斷。ClearInterrupted參數(shù)決定了是否清理interrupt狀態(tài)。
很好理解,調(diào)用isInterrupted(true)會(huì)返回當(dāng)前的interrupt狀態(tài),并清理interrupt狀態(tài)(即如果目前是中斷狀態(tài),會(huì)修改為非中斷狀態(tài))。
反之調(diào)用isInterrupted(false)不會(huì)清理interrupt狀態(tài)。
thread.interrupt()
源碼如前述,注釋大意如下:
中斷這個(gè)Thread對象。
線程可以中斷自身,但中斷其他線程需要通過checkAccess()方法檢查。(怎么檢查的沒細(xì)看)
如果線程在阻塞狀態(tài),會(huì)清除interrupt狀態(tài)并拋出異常,不同的阻塞類型會(huì)拋出不同的異常,比如wait()、sleep()等會(huì)拋出InterruptedException;否則,會(huì)設(shè)置interrupt狀態(tài)為中斷狀態(tài)。
源碼調(diào)用了native方法interrupt0(),我們就不更深入分析了。從注釋可以看出來線程是否在阻塞狀態(tài),會(huì)影響到interrupt()方法的行為:
- 如果線程在阻塞狀態(tài),這個(gè)方法會(huì)清除interrupt狀態(tài)并拋異常
- 如果線程未在阻塞狀態(tài),這個(gè)方法僅僅是設(shè)置了interrupt狀態(tài)
造成的影響
讓我們回到開頭的解決方案。
while(!中斷狀態(tài)) { // interrupt狀態(tài)
// do sth...
}
總的來說,我們面臨兩個(gè)問題:
- 如何合理獲取interrupt狀態(tài)?
-
Thread.interrupted()獲取并清除狀態(tài) -
thread.isInterrupted()獲取并不清除狀態(tài)
- 如果while循環(huán)中有阻塞邏輯,會(huì)不會(huì)導(dǎo)致我們的解決方案有差異?
- 阻塞時(shí)調(diào)用
thread.interrupt()方法會(huì)清理interrupt狀態(tài)并拋異常 - 非阻塞時(shí)調(diào)用
thread.interrupt()設(shè)置interrupt狀態(tài)并不拋異常
毫無疑問,獲取interrupt狀態(tài)兩種方法都可以。但某些錯(cuò)誤用法會(huì)導(dǎo)致線程無法中斷。
Bad Case1:錯(cuò)誤使用Thread.interrupted(),導(dǎo)致線程無法中斷
while(!Thread.interrupted()) { // interrupt狀態(tài)被清理,死循環(huán)
// do sth...
if (Thread.interrupted()) { // 這里的判斷清理了interrupt狀態(tài)
// 做一些中斷的后續(xù)動(dòng)作
}
}
這種Case在使用正則表達(dá)式匹配(matcher.find()方法)時(shí)也要注意。
而在調(diào)用thread.interrupt()方法并搭配獲取interrupt狀態(tài)的方法時(shí),就需要考慮阻塞問題了。
Bad Case2:未考慮阻塞導(dǎo)致interrupt狀態(tài)被吞,線程無法中斷
while(!Thread.interrupted()) { // interrupt狀態(tài)被吞,死循環(huán)
// do sth...
try {
// 阻塞時(shí)調(diào)用interrupt()方法,只拋異常不設(shè)置interrupt狀態(tài)
Thread.sleep(10);
} catch (InterruptedException e) {
// do sth...
}
}
Bad Case3:僅使用interrupt()方法并不能中斷線程
while(true) { // 死循環(huán)
// do sth...
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// do sth...
// 僅使用interrupt()方法,就像調(diào)用System.gc()一樣,只是進(jìn)行通知設(shè)置狀態(tài),并不表示動(dòng)作執(zhí)行
Thread.currentThread().interrupt();
}
}
實(shí)驗(yàn)驗(yàn)證
死循環(huán)3例
// Bad Case 1
public static void main(String[] args) throws Exception {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while(!Thread.interrupted() && i++ < 50000) { // interrupt狀態(tài)被清理,死循環(huán)
System.out.println(i);
if (Thread.interrupted()) { // 這里的判斷清理了interrupt狀態(tài)
System.out.println("interrupted");
}
}
}
});
thread.start();
Thread.sleep(5);
thread.interrupt(); // 中斷
}
// 輸出結(jié)果:輸出1~5w并在中間某處輸出了interrupted,說明中斷未生效,死循環(huán)
// Bad Case 2
public static void main(String[] args) throws Exception {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while(!Thread.interrupted() && i++ < 50) { // interrupt狀態(tài)被吞,死循環(huán)
System.out.println(i);
try {
// 阻塞時(shí)調(diào)用interrupt()方法,只拋異常不設(shè)置interrupt狀態(tài)
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
Thread.sleep(5);
thread.interrupt();
}
// 輸出結(jié)果:輸出1~50并在中間某處輸出了異常信息,說明中斷未生效,死循環(huán)
// Bad Case 3
public static void main(String[] args) throws Exception {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while(true && i++ < 50) { // 死循環(huán)
System.out.println(i);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
System.out.println("interrupted exception");
// 僅使用interrupt()方法,就像調(diào)用System.gc()一樣,只是進(jìn)行通知設(shè)置狀態(tài),并不表示動(dòng)作執(zhí)行
Thread.currentThread().interrupt();
}
}
}
});
thread.start();
Thread.sleep(5);
thread.interrupt();
}
// 輸出結(jié)果:從1~50中間開始輪番輸出數(shù)字和"interrupted exception",說明中斷未生效,死循環(huán)
驗(yàn)證結(jié)果符合預(yù)期。
正確的結(jié)束方式4例
// Case 1 無阻塞的情況
public static void main(String[] args) throws Exception {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while(!Thread.interrupted()) {
System.out.println(i++);
}
}
});
thread.start();
Thread.sleep(50);
thread.interrupt();
}
// 輸出結(jié)果:本機(jī)驗(yàn)證輸出0~11013,成功中斷
// Case 2 有阻塞的情況
public static void main(String[] args) throws Exception {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while(!Thread.interrupted()) {
System.out.println(i++);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
// 在這里調(diào)了一次interrupt(),保證線程未處于阻塞狀態(tài)
Thread.currentThread().interrupt();
}
}
}
});
thread.start();
Thread.sleep(50);
thread.interrupt();
}
// 輸出結(jié)果:本機(jī)驗(yàn)證輸出0~50并打印出InterruptedException,成功中斷
// Case 3 使用volatile變量控制線程同步
public class Test extends Thread {
// 利用volatile變量的機(jī)制
private volatile boolean stop;
@Override
public void run() {
int i = 0;
while(!stop) {
System.out.println(i++);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
System.out.println("interrupted exception");
}
}
}
public static void main(String[] args) throws Exception {
Test thread = new Test();
thread.start();
Thread.sleep(5);
thread.stop = true;
// 等5ms再調(diào)中斷方法,確認(rèn)在調(diào)interrupt方法時(shí)線程是否已中斷
Thread.sleep(5);
thread.interrupt();
}
}
// 輸出結(jié)果:本機(jī)驗(yàn)證輸出0~4,未輸出"interrupted exception",成功控制線程終止
// Case 4 無腦但安全
public static void main(String[] args) throws Exception {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
int i = 0;
while (true) {
System.out.println(i++);
Thread.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
Thread.sleep(50);
thread.interrupt();
}
// 輸出結(jié)果:本機(jī)驗(yàn)證輸出0~49并打印出InterruptedException,成功中斷
結(jié)論
- 調(diào)用thread.interrupt()方法不一定能中斷線程
- 阻塞狀態(tài)被中斷會(huì)拋異常,但不會(huì)設(shè)置interrupt狀態(tài)
- interrupt狀態(tài)設(shè)置為中斷不代表線程沒在運(yùn)行,類似System.gc()只是通知一下,main方法也可以中斷
- Thread.interrupted()方法會(huì)清理interrupt狀態(tài)
無阻塞的情況下要保證interrupt狀態(tài)僅在while判斷時(shí)重置,不能受其他部分影響。
while(!Thread.interrupted()) { // interrupt狀態(tài),要保證循環(huán)中不會(huì)重置這個(gè)值
// do sth...
}
有阻塞的情況下要保證interrupt狀態(tài)不被吞,可以在catch塊中再次調(diào)用interrupt()方法設(shè)置interrupt狀態(tài)。
while(!Thread.interrupted()) { // interrupt狀態(tài),要保證循環(huán)中不會(huì)重置這個(gè)值
// do sth...
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// do sth...
// 在這里調(diào)了一次interrupt(),此時(shí)線程未處于阻塞狀態(tài),會(huì)設(shè)置interrupt狀態(tài)
Thread.currentThread().interrupt();
}
}
還可以通過volatile變量控制線程中的循環(huán),不過這種方式略微麻煩了些,如果不了解原理還很容易錯(cuò),不太推薦使用。
當(dāng)然,還可以無腦try catch,視情況而定,不太推薦使用。
補(bǔ)充
如何確保JVM關(guān)停時(shí)終止該線程
上述Case中的線程,如果不刻意中斷,將會(huì)導(dǎo)致程序循環(huán),無法正常結(jié)束(比如Tomcat的shutdown過程無法停止),只能強(qiáng)行關(guān)停(kill -9)Java進(jìn)程。通過以下方法可以確保程序終止時(shí)終止該程序。
使用守護(hù)線程
就是調(diào)用thread.setDaemon(true)將線程設(shè)置為守護(hù)線程,會(huì)在主線程終止后自動(dòng)終止。
注意必須在線程啟動(dòng)前設(shè)置。
使用ShutdownHook
通過添加中斷邏輯到Hook中,可以在關(guān)閉程序(kill -15,ctrl+c)時(shí)運(yùn)行這些中斷邏輯。
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
if (!thread.isInterrupted()) {
thread.interrupt();
}
}
}));
不過直接調(diào)用thread.interrupt()都無法關(guān)閉的Bad Case,這種方式顯然也無法關(guān)閉。
參考資料
利用 java.lang.Runtime.addShutdownHook() 鉤子程序,保證java程序安全退出 - baibaluo - 博客園
本文搬自我的博客,歡迎參觀!