聊聊 Java 中的中斷機制
在Java中,用于終止一個正在運行中的線程,并非調(diào)用stop方法,而是自行設(shè)置一個標(biāo)志位,在安全點檢測標(biāo)志位,決定是否退出,但也可能會因為線程被掛起,無法走到標(biāo)志位。因此,Java線程提供了中斷機制,Thread類提供了中斷線程執(zhí)行的調(diào)用方法:interrupt,用于中斷因線程掛起的等待,調(diào)用interrupt方法后,線程會被喚醒,待下次cpu調(diào)度就會繼續(xù)執(zhí)行中斷后的代碼 。
我們經(jīng)常會調(diào)用Thread#sleep、Object#wait、Queue#poll等方法,并要求我們處理InterruptedException異常。 那么,拋出InterruptedException后,線程會終止嗎?
如果不捕獲InterruptedException,那么線程就會因為異常終止,是因為異常終止,并不是因為被中斷。如果捕獲了InterruptedException,那么線程就不會終止。
中斷,其實只是jvm用于喚醒因鎖競爭、I/O操作、休眠等待被掛起的線程,并設(shè)置一個中斷標(biāo)志,我們可以利用這個標(biāo)志去做一些處理。比如,當(dāng)我們發(fā)送消息給遠程服務(wù)器,并休眠等待結(jié)果時,如果線程被喚醒,并設(shè)置了中斷標(biāo)志,此時我們可以知道,并非等到結(jié)果被喚醒的,而是被中斷喚醒的,可以決定是繼續(xù)等待結(jié)果,還是放棄等待。
xxl-job提供取消任務(wù)操作,而任何運行中的線程,都只能利用中斷機制去結(jié)束線程任務(wù),所以我們想要任務(wù)支持被取消,那么在寫定時任務(wù)時,一定要考慮清楚,是不是應(yīng)該捕獲InterruptedException,如何利用中斷標(biāo)志結(jié)束任務(wù),否則將會導(dǎo)致任務(wù)無法被取消。
我們來看個案例:
- @Test
- public void test() {
- ExecutorService executorService = Executors.newSingleThreadExecutor();
- Future<?> future = executorService.submit(() -> {
- while (true) {
- System.out.println( "rung....." );
- ThreadUtils.sleep(1000);
- }
- });
- ThreadUtils.sleep(1000);
- future.cancel(true);
- try {
- future.get();
- } catch (InterruptedException | CancellationException | ExecutionException e) {
- e.printStackTrace();
- }
- ThreadUtils.sleep(1000 * 60);
- }
此案例創(chuàng)建了只有一個線程的線程池,提交了一個死循序任務(wù),該任務(wù)只調(diào)用ThreadUtils.sleep方法進入休眠。平常我們調(diào)用Thread.sleep方法都要求是否捕獲中斷異常,很多時候我們都會嫌棄麻煩,就用一個工具類提供sleep方法,然后將中斷異常捕獲,如ThreadUtils:
- public class ThreadUtils {
- public static void sleep(long millis) {
- try {
- Thread.sleep(millis);
- } catch (InterruptedException ignored) {
- }
- }
- }
此案例中,由于我們捕獲了中斷異常,因此這會導(dǎo)致任務(wù)并不會被終止,只是當(dāng)我們調(diào)用future的get方法時會拋出CancellationException異常,如下圖所示。
任務(wù)依然在運行中......
因此,在實際開發(fā)中,如果我們開發(fā)的Job也是如此,將會導(dǎo)致Job無法被中斷取消,直至Job執(zhí)行完成或者重啟。在開發(fā)Job時,應(yīng)當(dāng)合理考慮是否要捕獲中斷異常。
如果我們希望案例中的任務(wù)能夠被終止,我們可以這樣處理:
- @Test
- public void test() {
- ExecutorService executorService = Executors.newSingleThreadExecutor();
- Future<?> future = executorService.submit(() -> {
- while (true) {
- System.out.println( "rung....." );
- try {
- Thread.sleep(1000);
- } catch (InterruptedException ex) {
- System.err.println( "interrupted" );
- return; // 退出死循環(huán)
- }
- }
- });
- ThreadUtils.sleep(1000);
- future.cancel(true);
- try {
- future.get();
- } catch (InterruptedException | CancellationException | ExecutionException e) {
- e.printStackTrace();
- }
- ThreadUtils.sleep(1000 * 60);
- }
關(guān)于Thread的interrupt方法,注釋描述的大致意思如下:
- 如果被中斷的線程,當(dāng)前是調(diào)用Object#wait、Thread#join、Thread#sleep方法,將收到InterruptedException,并且會清除中斷標(biāo)志;
- 如果此線程在I/O操作中(指java nio)被阻塞,調(diào)用interrupt方法通道將被關(guān)閉,線程將收到一個ClosedByInterruptException,并且會設(shè)置中斷標(biāo)志;
- ....
怎么理解中斷標(biāo)志呢?
“如果被中斷的線程,當(dāng)前是調(diào)用Object#wait、Thread#join、Thread#sleep方法,將收到InterruptedException,并且會清除中斷標(biāo)志”,案例中的代碼正好符合這點,如果我們將案例代碼改為如下:
- @Test
- public void test() {
- ExecutorService executorService = Executors.newSingleThreadExecutor();
- Future<?> future = executorService.submit(() -> {
- while (!Thread.interrupted()) {
- System.out.println( "rung....." );
- try {
- Thread.sleep(1000);
- } catch (InterruptedException ex) {
- System.err.println( "interrupted" );
- }
- }
- });
- ThreadUtils.sleep(1000);
- future.cancel(true);
- try {
- future.get();
- } catch (InterruptedException | CancellationException | ExecutionException e) {
- e.printStackTrace();
- }
- ThreadUtils.sleep(1000 * 60);
- }
執(zhí)行這段代碼你會發(fā)現(xiàn),死循環(huán)根本沒有退出,正是因為Thread#sleep方法被中斷,JVM并不會設(shè)置中斷標(biāo)志,只是拋出InterruptedException異常。
其它情況下,JVM只會設(shè)置中斷標(biāo)志,并不會拋出InterruptedException。如果我們不處理中斷信號,那么中斷信號并不會影響程序的繼續(xù)執(zhí)行。
- @Test
- public void test2() {
- ExecutorService executorService = Executors.newSingleThreadExecutor();
- Future<?> future = executorService.submit(() -> {
- int number = 0;
- while (!Thread.interrupted()) {
- number++;
- }
- System.out.println(number);
- });
- ThreadUtils.sleep(1000);
- future.cancel(true);
- try {
- future.get();
- } catch (InterruptedException | CancellationException | ExecutionException e) {
- e.printStackTrace();
- }
- ThreadUtils.sleep(1000 * 60);
- }
此案例并沒有I/O操作導(dǎo)致的阻塞,因為調(diào)用中斷方法后,線程只是設(shè)置了中斷標(biāo)志,我們用中斷標(biāo)志作為循序的退出條件,運行此案例,我們將看到,線程中斷后,任務(wù)終止。反之,如果我們不處理中斷標(biāo)志,那么就等著IDEA進程卡掉吧。