Java中斷機制

zhong0316發表於2019-03-02

引言

Java中斷機制為我們提供了一種”試圖”停止一個執行緒的方法。設想我們有一個執行緒阻塞在一個耗時的I/O中,我們又不想一直等下去,那麼我們怎麼樣才能停止這個執行緒呢?答案就是Java的中斷機制。

從Java執行緒的狀態說起

Java執行緒的狀態包括:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING和TERMINATED總共六種狀態:

    public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }
複製程式碼

一個執行緒新建後,在呼叫該執行緒的start()方法之前,該執行緒的狀態就是NEW;當執行緒的start()方法被呼叫之後,該執行緒已經準備被CPU排程,此時執行緒的狀態就是RUNNABLE;如果一個物件的monitor lock被其他執行緒獲取了,這個時候我們再通過synchronized關鍵字去獲取改物件的monitor lock時,執行緒就會進入BLOCKED狀態;通過呼叫Thread#join()方法或者Object#wait()方法(不設定超時時間,with no timeout)或者LockSupport#park()方法可以讓一個執行緒從RUNNABLE狀態轉為WAITING狀態;TIMED_WAITING指執行緒處於等待中,但是這個等待是有期限的(),通過呼叫Thread#sleep(),Object#wait(long timeout),Thread#join(long timeout),LockSupport#parkNanos(),LockSupport#partUnitl()都可使執行緒切換到TIMED_WAITING狀態。最後一種狀態TERMINATED是指執行緒正常退出或者異常退出後的狀態。下面這張圖展示了這六種執行緒狀態之間的相互轉換關係:

執行緒狀態圖

當執行緒處於等待狀態或者有超時的等待狀態時(TIMED_WAITING,WAITING)我們可以通過呼叫執行緒的interrupt()方法來中斷執行緒的等待,此時執行緒會拋InterruptedException異常。例如Thread.sleep()方法:
public static native void sleep(long millis) throws InterruptedException;
此方法就會丟擲這類異常。
但是當執行緒處於BLOCKED狀態或者RUNNABLE(RUNNING)狀態時,呼叫執行緒的interrupt()方法也只能將執行緒的狀態位設定為true。停止執行緒的邏輯需要我們自己去實現。

中斷原理

檢視java原始碼可以看到Thread類中提供了跟中斷相關的一些方法:

    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();                                                 1

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {                                               2
                interrupt0();           // Just to set the interrupt flag  3
                b.interrupt(this);                                         4
                return;
            }
        }
        interrupt0();
    }
複製程式碼

interrupt()方法用於中斷一個執行緒,首先在第1處中斷方法會判斷當前caller執行緒是否具有中斷本執行緒的許可權,如果沒有許可權則丟擲SecurityException異常。然後在2處方法會判斷阻塞當前執行緒的物件是否為空,如果不為空,則在3處先設定執行緒的中斷flag為true,然後再由Interruptible物件(例如可以中斷的I/O操作)去中斷操作。從中我們也可以看到如果當前執行緒不處於WAITING或者TIMED_WAITING狀態,則interrupt()方法也只是僅僅設定了該執行緒的中斷flag為true而已。

    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
複製程式碼

再看interrupted()方法,該方法是一個靜態的方法,最終會呼叫具體執行緒例項的isInterrupted()方法:

    public boolean isInterrupted() {
        return isInterrupted(false);
    }
    
    private native boolean isInterrupted(boolean ClearInterrupted);
複製程式碼

interrupted()方法第一次呼叫時會返回當前執行緒的中斷flag,然後就清除了這個狀態位,但是isInterrupted()方法不會清除該狀態位。因此一個執行緒中斷後,第一次呼叫interrupted()方法會返回true,第二次呼叫就返回false,因此第一次呼叫後該狀態位已經被清除了。但是同樣一個執行緒中斷後,多次呼叫isInterrupted()方法都會返回true。這是兩者的不同之處,但是兩者最終都會呼叫一個名為isInterrupted的native方法。

中斷的處理

java中一般方法申明瞭throws InterruptedException的就是可以中斷的方法,比如:ReentrantLock#lockInterruptibly,BlockingQueue#put,Thread#sleep,Object#wait等。當這些方法被中斷後會丟擲InterruptedException異常,我們可以捕獲這個異常,並實現自己的處理邏輯,也可以不處理,繼續向上層丟擲異常。但是記住不要捕獲異常後什麼都不做並且不向上層拋異常,也就是說我們不能”吞掉“異常。
對於一些沒有丟擲InterruptedException的方法的中斷邏輯只能由我們自己去實現了。例如在一個大的迴圈中,我們可以自己判斷當前執行緒的中斷狀態然後選擇是否中斷當前操作:

public class InterruptTest {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                // 迴圈中檢測當前執行緒的中斷狀態 
                for (int i = 0; i < Integer.MAX_VALUE && !Thread.currentThread().isInterrupted(); i++) {
                    System.out.println(i);
                }
            }
        });
        thread.start();
        Thread.sleep(100);
        thread.interrupt();
    }
}
複製程式碼

相關文章