java多線程之中斷(interrupt)問題

摘要

在java中,想要讓一個線程停下來,有三種辦法:
(1)採用退出標誌,使得run方法執行完之後線程自然終止。
(2)使用stop強行終止線程,但該方法由於安全問題已經被deprecated。
(3)使用中斷機制。

引入

第一種方法沒特別之處,無非是在覆蓋Runnable接口之時對run方法中添加狀態標識邏輯。比如:

public class MyThread extends Thread
{
    private boolean running;
    @Override
    public run(){
         if(running){
             //....business methods
         }
    }
}

下面詳細討論中斷機制。
這裏的中斷是軟中斷,即在某個線程中可以將目標線程的中斷標誌位置位,然後在目標線程進行中斷檢查的時候來判斷自己應當如何響應中斷。
特別的,可以以此來停止目標線程。中斷的侷限性,無法對出於死鎖狀態的線程起作用。

簡單原理:

java.lang.Thread類中提供了一個方法來將線程的中斷標誌位置位:
Thread.interrupt()方法,其函數申明爲:public void interrupt();該方法會將線程的中斷標誌位置位。
java.lang.Thread類還兩個方法來檢查中斷標誌:

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

這兩個方法分別調用同一個原生方法來檢查中斷狀態並有所不同。
一方面interrupted()是靜態方法,可以使用Thread.interrupted()的方法來調用。
isInterrupted() 則需要一個實例對象作爲隱式參數,使用threadInstance.isInterrupted()來調用。
另一方面,interrupted在調用後會立即將線程中斷標誌位復位,而isInterrupted則不會這麼做。

非阻塞線程的中斷問題:

調用Thread.interrupt將目標線程中斷標誌位置位,然後在目標線程中檢查中斷標誌位。
如果檢查到中斷之後使用throw new InterruptedException()拋出一個異常,然後使用catch語句來捕獲這個異常,則可以在這裏完成一些中斷邏輯。
特別的,可以使用這種方法退出線程。下面是這種退出方式的舉例。

package com.kingbaron.jinterrupt;
/**
 * Created by kingbaron on 2016/3/17.
 */
public class UnblockingInterrupt extends Thread{
    @Override
    public void run(){
        super.run();
        try{
            for(int i=0;i<10000;i++)
            {
                if(Thread.interrupted())
                //if(this.isInterrupted())
                {
                    System.out.println("I'm interrupted and I'm going out");
                    throw new InterruptedException();
                }
                System.out.println("i="+(i+1));
            }
            System.out.println("I'm below the for clauses and I have no chance to run here");
        }catch(InterruptedException e){
            System.out.println("I'm in the catch clauses of the method UnblockingInterrupt.run()");
            e.printStackTrace();
        }
    }
}
package com.kingbaron.jinterrupt;
/**
 * Created by kingbaron on 2016/3/17.
 */
public class TestUnblockingInterrupt {
    public static void main(String[] args){
        try{
            UnblockingInterrupt thread=new UnblockingInterrupt();
            thread.start();
            Thread.sleep(2000);
            thread.interrupt();
        }catch(InterruptedException e)
        {
            System.out.println("main catch");
            e.printStackTrace();
        }
    }
}

輸出結果(關鍵部分)爲:

i=277919
i=277920
I'm interrupted and I'm going out
I'm in the catch clauses of the method UnblockingInterrupt.run
java.lang.InterruptedException
at com.kingbaron.jinterrupt.UnblockingInterrupt.run(UnblockingInterrupt.java:16)

可以看到,當線程thread的中斷標誌位被main線程中使用thread.interrupt置位後,在執行thread線程的for循環的if條件檢查時,
Thread.interrupted()返回true隨後在其中拋出一個InterruptedException異常,再使用catch子句捕獲該異常,由於run方法中在catch子句之後再無語句,
故線程從run方法返回,thread線程就此終止。

阻塞線程的中斷問題。

首先,一個線程出於阻塞中斷狀態(實際上還包括等待狀態)的原因,常見的有線程調用了thread.sleep、
thread.join、thread.wait、1.5中的condition.await、以及可中斷的通道上的 I/O 操作方法等。
如果一個線程處於阻塞狀態,若將其中斷標誌位置爲,則會在產生阻塞的語句出拋出一個InterruptedException異常,
並且在拋出異常後立即將線程的中斷標示位復位,即重新設置爲false。
拋出異常是爲了線程從阻塞狀態醒過來,並在結束線程前讓程序員有足夠的時間來處理中斷請求。
換言之,中斷將一個處於阻塞狀態的線程的阻塞狀態解除。
注意,synchronized在獲鎖的過程中是不能被中斷的,意思是說如果產生了死鎖,則不可能被中斷(請參考後面的測試例子)。
與synchronized功能相似的reentrantLock.lock()方法也是一樣,它也不可中斷的。
即如果發生死鎖,那麼reentrantLock.lock()方法無法終止,如果調用時被阻塞,則它一直阻塞到它獲取到鎖爲止。
但是如果調用帶超時的tryLock方法reentrantLock.tryLock(long timeout, TimeUnit unit),那麼如果線程在等待時被中斷,將拋出一個InterruptedException異常,這是一個非常有用的特性,因爲它允許程序打破死鎖。
你也可以調用reentrantLock.lockInterruptibly()方法,它就相當於一個超時設爲無限的tryLock方法。
沒有任何語言方面的需求一個被中斷的線程應該終止。中斷一個線程只是爲了引起該線程的注意,被中斷線程可以決定如何應對中斷。
某些線程非常重要,以至於它們應該不理會中斷,而是在處理完拋出的異常之後繼續執行,但是更普遍的情況是,一個線程將把中斷看作一個終止請求。
這種線程的run方法遵循如下形式:

public void run() {
    try {
        ...
        /*
         * 不管循環裏是否調用過線程阻塞的方法如sleep、join、wait,這裏還是需要加上
         * !Thread.currentThread().isInterrupted()條件,雖然拋出異常後退出了循環,顯
         * 得用阻塞的情況下是多餘的,但如果調用了阻塞方法但沒有阻塞時,這樣會更安全、更及時。
         */
        while (!Thread.currentThread().isInterrupted()&& more work to do) {
            do more work 
        }
    } catch (InterruptedException e) {
        //線程在wait或sleep期間被中斷了
    } finally {
        //線程結束前做一些清理工作
    }
}

在這裏,應當注意一個原則:儘量不要在底層代碼中捕獲InterruptedException原則。因爲該異常一旦被捕獲,線程的中斷標誌位就會被置位。
對於底層代碼而言,如果代碼會拋出不知道應當如何應對的InterruptException異常,可以有下面兩個選擇。
(1)捕獲到InterruptedException異常後將中斷標誌位置位,讓外層代碼根據檢查中斷標誌位來判斷是否中斷。

public subTask(){
    try{
        //...the works may throw a InterruptedException
    }catch(InterruptedException e){
        Thread.currentThread.interrupt();
    }
}

(2)更推薦的方法是,底層代碼不捕獲InterruptedException,直接將其拋給外層代碼去解決

public subTask() throws InterruptedException
{
    //...
}

中斷失效的情況之臨界區:

進入臨界區的代碼是不允許中斷的。這一點很好理解,臨界區是並行問題爲了保護臨界資源的互斥訪問而特地加鎖的,
一旦可以中斷,那麼鎖的存在也就沒有任何意義了。特別的,形成了死鎖的線程會分別處於阻塞狀態,但是它們都無法被中斷。
常見的有進入synchronized塊的代碼以及Lock.lock()之後沒能得到鎖而處於阻塞的狀態。通常可以使用Lock.lockInterruptibly()來代替Lock.lock(),
因爲Lock.lockInterruptibly()可以接受中斷,這個中斷指的是,既然沒能得到鎖進入臨界區,與其阻塞不如做些別的什麼事。
一旦進入臨界區,任何方法都不得中斷。

中斷失效的情況之不可中斷I/O:

自java 1.4之後對大量數據的I/O常用通道(channels)機制,它是可以被中斷的I/O,也就是說如果這種I/O處於阻塞狀態,可以使用中斷來解除阻塞。
但是存在着一些不可中斷的操作,比如ServerSocket.accept(),inputSteam.read()等調用interrupt()對於這幾個問題無效,因爲它們都不拋出中斷異常。如果拿不到資源,它們會無限期阻塞下去。
對於inputStream等資源,有些(實現了interruptibleChannel接口)可以通過close()方法將資源關閉,對應的阻塞也會被放開。
對於處理大型I/O時,推薦使用Channels。

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