如何優(yōu)雅地終止一個(gè)線程

背景

在開發(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è):

  1. Thread.interrupted()
  2. thread.isInterrupted()
  3. 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è)問題:

  1. 如何合理獲取interrupt狀態(tài)?
  • Thread.interrupted()獲取并清除狀態(tài)
  • thread.isInterrupted()獲取并不清除狀態(tài)
  1. 如果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é)論

  1. 調(diào)用thread.interrupt()方法不一定能中斷線程
  2. 阻塞狀態(tài)被中斷會(huì)拋異常,但不會(huì)設(shè)置interrupt狀態(tài)
  3. interrupt狀態(tài)設(shè)置為中斷不代表線程沒在運(yùn)行,類似System.gc()只是通知一下,main方法也可以中斷
  4. 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 - 博客園

本文搬自我的博客,歡迎參觀!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容