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