如何優雅的中斷線程

廢棄的做法-stop/suspend/resume

​ JDK1.0中定義了stop/suspend/resume方法,用於中止一個正在運行的線程;其中,stop用於徹底停止當前運行的線程,suspend是暫停當前運行線程,一直阻塞到其他線程調用resume方法。

​ 但是,JDK1.2開始,這幾個方法都被廢棄了,原因如下:

  1. stop方法會立即終止當前運行的線程,從而導致業務邏輯執行不完整。

  2. stop方法會立即釋放獨佔鎖資源,但是無法保證鎖內代碼塊的原子性。

    案例如下所示:

    public class ThreadTest {
    
        private Object object = new Object();
    
        private Integer i = 0;
    
        @Test
        public void test1() throws InterruptedException {
    
            Thread t1 = new Thread(()->{
                synchronized (object) {
                    i++;
                    System.out.println("i = " + i);
                    try {
                        TimeUnit.SECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    i--;
                    System.out.println("i = " + i);
                }
            });
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            t1.stop();
        }
    }
    

    上述代碼的邏輯很簡單,即主線程中開啓新的線程t1,t1中休眠10s,主線程休眠2s後調用stop方法,輸出的結果爲:

    i = 1;

    這對於業務來說是無法忍受的,如果stop方法之後要執行的邏輯是釋放資源等清理性的工作,那麼這些工作將永遠無法被執行;而且對於使用同步機制的業務來說,自然是想保證共享數據的一致性,但是由於stop方法顯然破壞了這一點。

  3. suspend和resume需要成對出現,否則極容易出現死鎖。線程在執行suspend方法後,並不會釋放鎖,此時如果另外一個線程需要先獲取該鎖資源再去resume目標線程,那麼就會發生死鎖。(如果想實現暫停-喚醒的邏輯,可以通過設置一個標誌位(volatile),表示該線程是應該運行還是掛起,如果是掛起,那麼就調用wait方法使其等待;如果該運行,即恢復,則調用notify重新啓動線程。wait方法與suspend不同的是,wait方法會釋放鎖資源,再被notify後需要重新參與鎖的競爭~)

中斷線程的方法

自定義標誌位

​ 通過自定義變量作爲狀態位,定期檢查該變量,符合條件則繼續執行,否則退出。該方法適應於正在運行的線程

​ 將一個大任務分割爲多個小任務,每次小任務執行完成後都會去校驗一個狀態標誌位是否爲true,如果非true,那麼線程終止,不再繼續執行任務。(狀態標誌位爲volatile,保證可見性)

public class ThreadTest2 {

    private static volatile boolean isRun = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            int batch = 0;
            while (isRun) {
                System.out.println("==========" + batch++);
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        //終止線程
        stop();
        TimeUnit.SECONDS.sleep(10000);
    }

    public static void stop() {
        isRun = false;
    }
}

Interrupt()方法

​ 該方法適應於非正在運行線程的退出,對於大部分阻塞線程的方法,通過Thread.interrupt()可以立刻退出等待,拋出InterruptedException,包含sleep、join、wait和Lock.lockInterruptibly()、NIO阻塞等;

​ 如果你用了線程池,並使用了Future對象,可以使用future.cancel(boolean)方法取消正在執行的任務,false會等待正在執行的任務執行完成,但是會取消還沒開始執行的任務;true表示會中斷正在執行且能夠響應中斷異常的任務!!

案例

​ case1:一直運行的線程無法被中斷

    @Test
    public void test0() throws InterruptedException {
        Thread t1 = getThread0();
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();
        System.out.println("thread isInterrupted = " + t1.isInterrupted());
        TimeUnit.SECONDS.sleep(10);
    }
    public Thread getThread0() {
        return new Thread(() -> {
            int count = 0;
            while (true) {
                System.out.println("=======" + count++);
            }
        });
    }

​ 運行結果:一直運行,直至10s後test1方法結束;

​ case2:線程被中斷

    @Test
    public void test1() throws InterruptedException {
        Thread t2 = getThread1();
        t2.start();
        TimeUnit.SECONDS.sleep(2);
        t2.interrupt();
        System.out.println("thread isInterrupted = " + t2.isInterrupted());
        TimeUnit.SECONDS.sleep(10);
    }
    public Thread getThread1() {
        return new Thread(() -> {
            int count = 0;
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("==========" + count++);
            }
            System.out.println(Thread.currentThread().getName() + " is interrupted");
        });
    }

​ 運行結果:調用完interrupt方法後,線程中斷結束;

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();
    }

​ 首先判斷執行中斷的線程是不是自身被中斷的線程,如果是由其他線程進行中斷,會調用checkAccess()方法進行校驗其他線程是否有權限對當前線程進行修改,源碼如下:

    public final void checkAccess() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkAccess(this);
        }
    }

​ SecurityManager是java提供的安全管理器,目的是在運行階段檢查應用訪問資源的權限,保護系統免受惡意操作攻擊。可以通過jvm配置或者編碼方式啓動安全管理器,若沒有指定policy文件,那麼會加載默認的策略文件。在策略文件中,我們可以定義系統文件讀取權限、序列化權限等。

​ 正常情況下,應用是沒有啓動安全管理器的,所以System.getSecurityManager() == null,直接跳過檢查;

​ 根據Interruptible b 是否爲null,分爲兩部分邏輯處理,但是都會調用本地方法interrupt0;下面分開說明兩部分的邏輯。

Interruptible b != null

​ blocker爲Thread的成員變量,並提供了blockedOn()方法設置blocker的值;

   void blockedOn(Interruptible b) {
        synchronized (blockerLock) {
            blocker = b;
        }
    }

​ 傳統IO在讀寫時,雖然是阻塞的,但是無法被中斷;NIO支持在讀寫操作時進行中斷,channel若實現了InterruptiableChannel接口,則表示支持中斷。如常用的FileChannel,SocketChannel,DatagramChannel等都實現了該接口。

​ 在其子類AbstractInterruptibleChannel中,定義了實現可中斷IO機制的方法begin和end;NIO規定,在阻塞IO的語句前後,需要調用begin和end方法,爲了保證end方法一定被調用,要求放在finally塊中;

begin方法如下:

protected final void begin() {
        if (interruptor == null) {
            //初始化中斷處理對象,中斷處理對象中提供回調機制
            interruptor = new Interruptible() {
                    public void interrupt(Thread target) {
                        synchronized (closeLock) {
                            if (!open)
                                return;
                            //設置標誌位
                            open = false;
                            interrupted = target;
                            try {
                                //關閉channel
                                AbstractInterruptibleChannel.this.implCloseChannel();
                            } catch (IOException x) { }
                        }
                    }};
        }
   	    //將中斷處理對象註冊到當前線程
        blockedOn(interruptor);
        Thread me = Thread.currentThread();
    	//當前線程如果被中斷,則註冊的中斷處理對象可能沒有被執行,手動觸發一下
        if (me.isInterrupted())
            interruptor.interrupt(me);
    }

​ 總的來說,就是在Thread的中斷邏輯中,掛載自定義的中斷處理對象,這樣Thread對象被中斷時,會執行中斷處理對象的回調,在回調中執行關閉channel操作,這樣就實現了對線程中斷的響應。

​ Thread添加中斷處理邏輯 是依賴blockedOn方法,如下:

static void blockedOn(Interruptible intr) {         // package-private
        sun.misc.SharedSecrets.getJavaLangAccess().blockedOn(Thread.currentThread(),
                                                             intr);
    }

​ 這裏用到了SharedSecrets類,通過SharedSecrets能夠訪問JDK類庫中因爲作用域的限制而無法訪問的類或者方法(和反射實現的效果一樣),該方法實際上就調用thread.blockedOn(interruptible)方法。

​ 回過來看看interrupt方法,在 b != null 時,首先調用interrupt0()方法,該方法只是設置interrupt標誌位,然後調用b.interrupt方法,該方法就是上述說明的回調方法,一清二楚啦~

Interruptible b == null

​ 對於b==null時,會直接調用本地方法interrupt0(),該方法能夠中斷wait,sleep和join等阻塞等待的方法;

​ 對於java線程來說,最終都會映射爲操作系統的線程。當執行interrupt()方法後,如果操作系統線程沒有被中斷,那麼會設置操作系統線程的interrupt標誌位爲true,並喚醒線程的SleepEvent,隨後喚醒線程的parker和ParkEvent。

​ ParkEvent包含了_ParkEvent變量和_SleepEvent變量,其中,_ParkEvent變量用於synchronized同步塊和Object.wait()方法,_SleepEvent變量用於Thead.sleep();ParkEvent中包含了一把mutex互斥鎖和一個cond條件變量,線程的阻塞和喚醒(park和unpark)通過它們實現的。

  • PlatformEvent::park() 方法會調用庫函數pthread_cond_wait(_cond, _mutex)實現線程等待

    • synchronized塊的進入和Object.wait()的線程等待都是通過PlatformEvent::park()方法實現
    • 注:Thread.join()是使用的Object.wait()實現的
  • PlatformEvent::park(jlong millis)方法會調用庫函數pthread_cond_timedwait(_cond, _mutex, _abstime)實現計時條件等待

    • Thread.sleep(millis)就是通過PlatformEvent::park(jlong millis)實現
  • PlatformEvent::unpark()方法會調用庫函數pthread_cond_signal (_cond)喚醒上述等待的條件變量

    • Thread.interrupt()就會觸發其子類SleepEvent和ParkEvent的unpark()方法
    • synchronized塊的退出也會觸發unpark()。其所在對象ObjectMonitor維護了ParkEvent數組作爲喚醒隊列,synchronized同步塊退出時,會觸發ParkEvent::unpark()方法來喚醒等待進入同步塊的線程,或等待在Object.wait()的線程。

​ 對於Synchronized等待事件,被喚醒後會嘗試獲取鎖,如果失敗則會通過循環繼續park()等待,因此synchronized等待實際上不會被中斷的;如果是Object.wait()事件,則會通過標記從而判斷是否爲notify()喚醒,如果不是則拋出InterruptedExcetion進行中斷。

​ Parker與上述的ParkEvent類似,也持有一把mutex互斥鎖和一個cond條件變量;凡是在java代碼中通過unsafe.park()/unpark()的調用都會映射到Thread的_parker變量去執行。而unsafe.park()/unpark()正是由LockSupport類調用,如ReentrantLock,CountDownLatch,Semaphore,ThreadPoolExecutor,ArrayBlockingQueue等。(LockSupport.park()和unpark()類似於Object.wait和notify方法,不同的是,它不需要在同步代碼塊中,且unpark即便在park方法前進行調用,依然能夠喚醒線程)

​ 並非所有的阻塞方法都拋出 InterruptedException。輸入和輸出流類會阻塞等待 I/O 完成,但是它們不拋出 InterruptedException,而且在被中斷的情況下也不會提前返回。然而,對於套接字 I/O,如果一個線程關閉套接字,則那個套接字上的阻塞 I/O 操作將提前結束,並拋出一個 SocketExceptionjava.nio 中的非阻塞 I/O 類也不支持可中斷 I/O,但是同樣可以通過關閉通道或者請求 Selector 上的喚醒來取消阻塞操作。類似地,嘗試獲取一個內部鎖的操作(進入一個 synchronized 塊)是不能被中斷的,但是 ReentrantLock 支持可中斷的獲取模式。

如何處理中斷異常-InterruptedException

  1. 如果自己無法處理異常,可以在方法聲明中向外拋出異常(throws InterruptedException),交給上層具體的業務來處理;
  2. 先捕獲異常,做一些清理工作,然後再向外拋出異常;比如,玩家匹配遊戲,當一個玩家匹配已經到來,但是另外一個玩家未到來,發生了中斷,此時需要把已經到來的玩家重新放回隊列中,然後再拋出異常;
public class PlayerMatcher {
    private PlayerSource players;
 
    public PlayerMatcher(PlayerSource players) { 
        this.players = players; 
    }
 
    public void matchPlayers() throws InterruptedException { 
        try {
             Player playerOne, playerTwo;
             while (true) {
                 playerOne = playerTwo = null;
                 // Wait for two players to arrive and start a new game
                 playerOne = players.waitForPlayer(); // could throw IE
                 playerTwo = players.waitForPlayer(); // could throw IE
                 startNewGame(playerOne, playerTwo);
             }
         }
         catch (InterruptedException e) {  
             // If we got one player and were interrupted, put that player back
             if (playerOne != null)
                 players.addFirst(playerOne);
             // Then propagate the exception
             throw e;
         }
    }
}
  1. 如果自己知道發生異常後如何進行處理,那麼不應該向外拋出異常。比如當由Runnable定義的任務調用了一個可中斷的方法時,那麼當發生中斷異常時是ok的,但是並不意味着捕獲異常然後什麼都不做,因爲java中在檢測到中斷並拋出InterruptedException時,會清除中斷狀態(保證只能被中斷一次);因此需要保留中斷髮生的證據,以便調用棧中更高層的代碼能夠知道中斷,並對中斷做出響應;可以通過interrupt()方法進行 重新中斷 來完成。
public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue;
 
    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    }
 
    public void run() { 
        try {
             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         }
         catch (InterruptedException e) { 
             // Restore the interrupted status
             Thread.currentThread().interrupt();
         }
    }
}
  1. 吞掉中斷異常或者log一下,這也是最常見的處理方式,不推薦;
public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue;
 
    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    }
 
    public void run() { 
        try {
             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         }
         catch (InterruptedException swallowed) { 
             /* DON'T DO THIS - RESTORE THE INTERRUPTED STATUS INSTEAD */
         }
    }
}
  1. 如果不能重新拋出 InterruptedException,不管您是否計劃處理中斷請求,仍然需要重新中斷當前線程,因爲一箇中斷請求可能有多個 “接收者”。標準線程池 (ThreadPoolExecutor)worker 線程實現負責中斷,因此中斷一個運行在線程池中的任務可以起到雙重效果,一是取消任務,二是通知執行線程線程池正要關閉。如果任務生吞中斷請求,則 worker 線程將不知道有一個被請求的中斷,從而耽誤應用程序或服務的關閉。
  2. 對於不可取消的任務,需要在合適的時候重新設置中斷狀態。有些任務拒絕被中斷,這使得它們是不可取消的。但是,即使是不可取消的任務也應該嘗試保留中斷狀態,以防在不可取消的任務結束之後,調用棧上更高層的代碼需要對中斷進行處理。清單 6 展示了一個方法,該方法等待一個阻塞隊列,直到隊列中出現一個可用項目,而不管它是否被中斷。爲了方便他人,它在結束後在一個 finally 塊中恢復中斷狀態,以免剝奪中斷請求的調用者的權利。(它不能在更早的時候恢復中斷狀態,因爲那將導致無限循環 —— BlockingQueue.take() 將在入口處立即輪詢中斷狀態,並且,如果發現中斷狀態集,就會拋出 InterruptedException。)
public Task getNextTask(BlockingQueue<Task> queue) {
    boolean interrupted = false;
    try {
        while (true) {
            try {
                return queue.take();
            } catch (InterruptedException e) {
                interrupted = true;
                // fall through and retry
            }
        }
    } finally {
        if (interrupted)
            Thread.currentThread().interrupt();
    }
}

Interrupted()和isInterrupted()

public static boolean interrupted() {
    return currentThread().isInterrupted(true);//本地方法,true清除中斷狀態
}
public boolean isInterrupted() {
    return isInterrupted(false); //本地方法,false不會清除中斷狀態
}

​ 兩個方法都能查看線程的中斷狀態,區別在於,interrupted()是static方法,調用後會清除線程的中斷狀態;而isInterrupted()是普通方法,調用後不會清除線程的中斷狀態。

線程池優雅關閉

​ 該部分內容會在下個章節-優雅停機 部分進行講解,敬請期待~

參考

Thread.interrupt相關源碼分析

線程中斷機制

java理論與實踐:處理InterruptedException

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章