Thread類源碼解讀(3)——線程中斷interrupt

前言

系列文章目錄java

線程中斷是一個很重要的概念,一般,取消一個任務的執行,最好的,同時也是最合理的方法,就是經過中斷。segmentfault

本篇咱們主要仍是經過源碼分析來看看中斷的概念。安全

本文的源碼基於JDK1.8less

Interrupt status & InterruptedException

java線程的中斷機制爲咱們提供了一個契機,使被中斷的線程可以有機會從當前的任務中跳脫出來。而中斷機制的最核心的兩個概念就是interrupt statusInterruptedExceptionide

java中對於中斷的大部分操做無外乎如下兩點:函數

  1. 設置或者清除中斷標誌位
  2. 拋出InterruptedException

interrupt status

在java中,每個線程都有一箇中斷標誌位,表徵了當前線程是否處於被中斷狀態,咱們能夠把這個標識位理解成一個boolean類型的變量,當咱們中斷一個線程時,將該標識位設爲true,當咱們清除中斷狀態時,將其設置爲false, 其僞代碼以下:源碼分析

(注意,本文的僞代碼部分是我我的所寫,並不權威,只是幫助我本身理解寫的)this

// 注意,這是僞代碼!!!
// 注意,這是僞代碼!!!
// 注意,這是僞代碼!!!
public class Thread implements Runnable {
    private boolean interruptFlag; // 中斷標誌位
    
    public boolean getInterruptFlag() {
        return this.interruptFlag;
    }
    
    public void setInterruptFlag(boolean flag) {
        this.interruptFlag = flag;
    }
}

然而,在Thread線程類裏面,並無相似中斷標誌位的屬性,可是提供了獲取中斷標誌位的接口:線程

/**
 * Tests if some Thread has been interrupted.  The interrupted state
 * is reset or not based on the value of ClearInterrupted that is
 * passed.
 */
private native boolean isInterrupted(boolean ClearInterrupted);

這是一個native方法,同時也是一個private方法,該方法除了可以返回當前線程的中斷狀態,還能根據ClearInterrupted參數來決定要不要重置中斷標誌位(reset操做至關於上面的interruptFlag = false)。code

Thread類提供了兩個public方法來使用該native方法:

public boolean isInterrupted() {
    return isInterrupted(false);
}

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

其中isInterrupted調用了isInterrupted(false), ClearInterrupted參數爲false, 說明它僅僅返回線程實例的中斷狀態,可是不會對現有的中斷狀態作任何改變,僞代碼能夠是:

// 注意,這是僞代碼!!!
// 注意,這是僞代碼!!!
// 注意,這是僞代碼!!!
public boolean isInterrupted() {
    return interruptFlag; //直接返回Thread實例的中斷狀態
}

interrupted是一個靜態方法,因此它能夠由Thread類直接調用,天然就是做用於當前正在執行的線程,因此函數內部使用了currentThread()方法,與isInterrupted()方法不一樣的是,它的ClearInterrupted參數爲true,在返回線程中斷狀態的同時,重置了中斷標識位,僞代碼能夠是:

// 注意,這是僞代碼!!!
// 注意,這是僞代碼!!!
// 注意,這是僞代碼!!!
public static boolean interrupted() {
    Thread current = Thread.currentThread(); // 獲取當前正在執行的線程
    boolean interruptFlag = current.getInterruptFlag(); // 獲取線程的中斷狀態
    current.setInterruptFlag(false); // 清除線程的中斷狀態
    return interruptFlag; //返回線程的中斷狀態
}

可見,isInterrupted()interrupted() 方法只涉及到中斷狀態的查詢,最可能是多加一步重置中斷狀態,並不牽涉到InterruptedException

不過值得一提的是,在咱們能使用到的public方法中,interrupted()是咱們清除中斷的惟一方法。

InterruptedException

咱們直接來看的源碼:

/**
 * Thrown when a thread is waiting, sleeping, or otherwise occupied,
 * and the thread is interrupted, either before or during the activity.
 * Occasionally a method may wish to test whether the current
 * thread has been interrupted, and if so, to immediately throw
 * this exception.  The following code can be used to achieve
 * this effect:
 * <pre>
 *  if (Thread.interrupted())  // Clears interrupted status!
 *      throw new InterruptedException();
 * </pre>
 *
 * @author  Frank Yellin
 * @see     java.lang.Object#wait()
 * @see     java.lang.Object#wait(long)
 * @see     java.lang.Object#wait(long, int)
 * @see     java.lang.Thread#sleep(long)
 * @see     java.lang.Thread#interrupt()
 * @see     java.lang.Thread#interrupted()
 * @since   JDK1.0
 */
public class InterruptedException extends Exception {
    private static final long serialVersionUID = 6700697376100628473L;

    /**
     * Constructs an <code>InterruptedException</code> with no detail  message.
     */
    public InterruptedException() {
        super();
    }

    /**
     * Constructs an <code>InterruptedException</code> with the
     * specified detail message.
     *
     * @param   s   the detail message.
     */
    public InterruptedException(String s) {
        super(s);
    }
}

上面的註釋是說,在線程處於「waiting, sleeping」甚至是正在運行的過程當中,若是被中斷了,就能夠拋出該異常,咱們先來回顧一下咱們前面遇到過的拋出InterruptedException異常的例子:

(1) wait(long timeout)方法中的InterruptedException

/* 
 *
 * @param      timeout   the maximum time to wait in milliseconds.
 * @throws  IllegalArgumentException      if the value of timeout is
 *               negative.
 * @throws  IllegalMonitorStateException  if the current thread is not
 *               the owner of the object's monitor.
 * @throws  InterruptedException if any thread interrupted the
 *             current thread before or while the current thread
 *             was waiting for a notification.  The <i>interrupted
 *             status</i> of the current thread is cleared when
 *             this exception is thrown.
 * @see        java.lang.Object#notify()
 * @see        java.lang.Object#notifyAll()
 */
public final native void wait(long timeout) throws InterruptedException;

該方法的註釋中提到,若是在有別的線程在當前線程進入waiting狀態以前或者已經進入waiting狀態以後中斷了當前線程,該方法就會拋出InterruptedException,同時,異常拋出後,當前線程的中斷狀態也會被清除。

(2) sleep(long millis)方法中的InterruptedException

/* @param  millis
 *         the length of time to sleep in milliseconds
 *
 * @throws  IllegalArgumentException
 *          if the value of {@code millis} is negative
 *
 * @throws  InterruptedException
 *          if any thread has interrupted the current thread. The
 *          <i>interrupted status</i> of the current thread is
 *          cleared when this exception is thrown.
 */
public static native void sleep(long millis) throws InterruptedException;

與上面的wait方法一致,若是當前線程被中斷了,sleep方法會拋出InterruptedException,而且清除中斷狀態。

若是有其餘方法直接或間接的調用了這兩個方法,那他們天然也會在線程被中斷的時候拋出InterruptedException,而且清除中斷狀態。例如:

  • wait()
  • wait(long timeout, int nanos)
  • sleep(long millis, int nanos)
  • join()
  • join(long millis)
  • join(long millis, int nanos)

這裏值得注意的是,雖然這些方法會拋出InterruptedException,可是並不會終止當前線程的執行,當前線程能夠選擇忽略這個異常。

也就是說,不管是設置interrupt status 仍是拋出InterruptedException,它們都是給當前線程的建議,當前線程能夠選擇採納或者不採納,它們並不會影響當前線程的執行。

至於在收到這些中斷的建議後,當前線程要怎麼處理,也徹底取決於當前線程。

interrupt

上面咱們說了怎麼檢查(以及清除)一個線程的中斷狀態,提到當一個線程被中斷後,有一些方法會拋出InterruptedException。

下面咱們就來看看怎麼中斷一個線程。

要中斷一個線程,只需調用該線程的interrupt方法,其源碼以下:

/**
 * Interrupts this thread.
 *
 * <p> Unless the current thread is interrupting itself, which is
 * always permitted, the {@link #checkAccess() checkAccess} method
 * of this thread is invoked, which may cause a {@link
 * SecurityException} to be thrown.
 *
 * <p> If this thread is blocked in an invocation of the {@link
 * Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
 * Object#wait(long, int) wait(long, int)} methods of the {@link Object}
 * class, or of the {@link #join()}, {@link #join(long)}, {@link
 * #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
 * methods of this class, then its interrupt status will be cleared and it
 * will receive an {@link InterruptedException}.
 *
 * <p> If this thread is blocked in an I/O operation upon an {@link
 * java.nio.channels.InterruptibleChannel InterruptibleChannel}
 * then the channel will be closed, the thread's interrupt
 * status will be set, and the thread will receive a {@link
 * java.nio.channels.ClosedByInterruptException}.
 *
 * <p> If this thread is blocked in a {@link java.nio.channels.Selector}
 * then the thread's interrupt status will be set and it will return
 * immediately from the selection operation, possibly with a non-zero
 * value, just as if the selector's {@link
 * java.nio.channels.Selector#wakeup wakeup} method were invoked.
 *
 * <p> If none of the previous conditions hold then this thread's interrupt
 * status will be set. </p>
 *
 * <p> Interrupting a thread that is not alive need not have any effect.
 *
 * @throws  SecurityException
 *          if the current thread cannot modify this thread
 *
 * @revised 6.0
 * @spec JSR-51
 */
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();
}

上面的註釋很長,咱們一段一段來看:

/**
 * Interrupts this thread.
 *
 * <p> Unless the current thread is interrupting itself, which is
 * always permitted, the {@link #checkAccess() checkAccess} method
 * of this thread is invoked, which may cause a {@link
 * SecurityException} to be thrown.
 ...
 */

上面這段首先說明了這個函數的目的是中斷這個線程,這個this thread,固然指的就是該方法所屬的線程對象所表明的線程。

接着說明了,一個線程老是被容許中斷本身,可是咱們若是想要在一個線程中中斷另外一個線程的執行,就須要先經過checkAccess()檢查權限。這有可能拋出SecurityException異常, 這段話用代碼體現爲:

if (this != Thread.currentThread())
    checkAccess();

咱們接着往下看:

/*
 ...
 * <p> If this thread is blocked in an invocation of the {@link
 * Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
 * Object#wait(long, int) wait(long, int)} methods of the {@link Object}
 * class, or of the {@link #join()}, {@link #join(long)}, {@link
 * #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
 * methods of this class, then its interrupt status will be cleared and it
 * will receive an {@link InterruptedException}.
 ...
 */

上面這段是說,若是線程由於如下方法的調用而處於阻塞中,那麼(調用了interrupt方法以後),線程的中斷標誌會被清除,而且收到一個InterruptedException:

  • Object的方法

    • wait()
    • wait(long)
    • wait(long, int)
  • Thread的方法

    • join()
    • join(long)
    • join(long, int)
    • sleep(long)
    • sleep(long, int)

關於這一點,咱們上面在分析InterruptedException的時候已經分析過了。

這裏插一句,因爲上面這些方法在拋出InterruptedException異常後,會同時清除中斷標識位,所以當咱們此時不想或者沒法傳遞InterruptedException異常,也不對該異常作任何處理時,咱們最好經過再次調用interrupt來恢復中斷的狀態,以供上層調用者處理,這一點,咱們在逐行分析AQS源碼(二): 鎖的釋放的最後就說明過這種用法。

接下來的兩段註釋是關於NIO的,咱們暫時不看,直接看最後兩段:

/*
 ...
 * <p> If none of the previous conditions hold then this thread's interrupt
 * status will be set. </p>
 *
 * <p> Interrupting a thread that is not alive need not have any effect.
 */

這段話是說:

  • 若是線程沒有由於上面的函數調用而進入阻塞狀態的話,那麼中斷這個線程僅僅會設置它的中斷標誌位(而不會拋出InterruptedException)
  • 中斷一個已經終止的線程不會有任何影響。

註釋看完了以後咱們再來看代碼部分,其實代碼部分很簡單,中間那段同步代碼塊是和NIO有關的,咱們能夠暫時無論,整個方法的核心調用就是interrupt0()方法,而它是一個native方法:

private native void interrupt0();

這個方法所作的事情很簡單:

Just to set the interrupt flag

因此,至此咱們明白了,所謂「中斷一個線程」,其實並非讓一個線程中止運行,僅僅是將線程的中斷標誌設爲true, 或者在某些特定狀況下拋出一個InterruptedException,它並不會直接將一個線程停掉,在被中斷的線程的角度看來,僅僅是本身的中斷標誌位被設爲true了,或者本身所執行的代碼中拋出了一個InterruptedException異常,僅此而已。

終止一個線程

既然上面咱們提到了,中斷一個線程並不會使得該線程中止執行,那麼咱們該怎樣終止一個線程的執行呢。早期的java中提供了stop()方法來中止一個線程,可是這個方法是不安全的,因此已經被廢棄了。如今終止一個線程,基本上只能靠「曲線救國」式的中斷來實現。

終止處於阻塞狀態的線程

前面咱們說過,當一個線程由於調用wait,sleep,join方法而進入阻塞狀態後,若在這時中斷這個線程,則這些方法將會拋出InterruptedException異常,咱們能夠利用這個異常,使線程跳出阻塞狀態,從而終止線程。

@Override
public void run() {
    while(true) {
        try {
            // do some task
            // blocked by calling wait/sleep/join
        } catch (InterruptedException ie) {  
            // 若是該線程被中斷,則會拋出InterruptedException異常
            // 咱們經過捕獲這個異常,使得線程從block狀態退出
            break; // 這裏使用break, 可使咱們在線程中斷後退出死循環,從而終止線程。
        }
    }
}

終止處於運行狀態的線程

與中斷一個處於阻塞狀態所不一樣的是,中斷一個處於運行狀態的線程只會將該線程的中斷標誌位設爲true, 而並不會拋出InterruptedException異常,爲了能在運行過程當中感知到線程已經被中斷了,咱們只能經過不斷地檢查中斷標誌位來實現:

@Override
public void run() {
    while (!isInterrupted()) {
        // do some task...
    }
}

這裏,咱們每次循環都會先檢查中斷標誌位,只要當前線程被中斷了,isInterrupted()方法就會返回true,從而終止循環。

終止一個Alive的線程

上面咱們分別介紹了怎樣終止一個處於阻塞狀態或運行狀態的線程,若是咱們將這兩種方法結合起來,那麼就能夠同時應對這兩種情況,從而可以終止任意一個存活的線程:

@Override
public void run() {
    try {
        // 1. isInterrupted() 用於終止一個正在運行的線程。
        while (!isInterrupted()) {
            // 執行任務...
        }
    } catch (InterruptedException ie) {  
        // 2. InterruptedException異經常使用於終止一個處於阻塞狀態的線程
    }
}

不過使用這二者的組合必定要注意,wait,sleep,join等方法拋出InterruptedException有一個反作用: 清除當前的中斷標誌位,因此不要在異常拋出後不作任何處理,而寄望於用isInterrupted()方法來判斷,由於中標誌位已經被重置了,因此下面這種寫法是不對的:

@Override
public void run() {
    //isInterrupted() 用於終止一個正在運行的線程。
    while (!isInterrupted()) {
        try {
                // 執行任務...
            }
        } catch (InterruptedException ie) {  
            // 在這裏不作任何處理,僅僅依靠isInterrupted檢測異常
        }
    }
}

這個方法中,在catch塊中咱們檢測到異常後沒有使用break方法跳出循環,而此時中斷狀態已經被重置,當咱們再去調用isInterrupted,依舊會返回false, 故線程仍然會在while循環中執行,沒法被中斷。

總結

Java沒有提供一種安全直接的方法來中止某個線程,可是提供了中斷機制。對於被中斷的線程,中斷只是一個建議,至於收到這個建議後線程要採起什麼措施,徹底由線程本身決定。

中斷機制的核心在於中斷狀態和InterruptedException異常

中斷狀態:

  • 設置一箇中斷狀態: Thread#interrupt
  • 清除一箇中斷狀態: Thread.interrupted

Thread.interrupted方法同時會返回線程原來的中斷的狀態。
若是僅僅想查看線程當前的中斷狀態而不清除原來的狀態,則應該使用Thread#isInterrupted。

某些阻塞方法在拋出InterruptedException異常後,會同時清除中斷狀態。若不能對該異常作出處理也沒法向上層拋出,則應該經過再次調用interrupt方法恢復中斷狀態,以供上層處理,一般狀況下咱們都不該該屏蔽中斷請求。

中斷異常:

中斷異常通常是線程被中斷後,在一些block類型的方法(如wait,sleep,join)中拋出。

咱們可使用Thread#interrupt中斷一個線程,被中斷的線程所受的影響爲如下兩種之一:

  • 若被中斷前,該線程處於非阻塞狀態,那麼該線程的中斷狀態被設爲true, 除此以外,不會發生任何事。
  • 若被中斷前,該線程處於阻塞狀態(調用了wait,sleep,join等方法),那麼該線程將會當即從阻塞狀態中退出,並拋出一個InterruptedException異常,同時,該線程的中斷狀態被設爲false, 除此以外,不會發生任何事。

不管是中斷狀態的改變仍是InterruptedException被拋出,這些都是當前線程能夠感知到的"建議",若是當前線程選擇忽略這些建議(例如簡單地catch住異常繼續執行),那麼中斷機制對於當前線程就沒有任何影響,就好像什麼也沒有發生同樣。

因此,中斷一個線程,只是傳遞了請求中斷的消息,並不會直接阻止一個線程的運行。

(完)

查看更多系列文章:系列文章目錄

相關文章
相關標籤/搜索