[轉(zhuǎn)載]線程的中斷(interrupt)機制

什么時候需要關(guān)閉一個線程咆耿?

下面簡單的舉例情況:

  • 比如我們會啟動多個線程做同一件事,比如搶12306的火車票,我們可能開啟多個線程從多個渠道買火車票呆馁,只要有一個渠道買到了,我們會通知取消其他渠道毁兆。這個時候需要關(guān)閉其他線程
  • 很多線程的運行模式是死循環(huán)浙滤,比如在生產(chǎn)者/消費者模式中,消費者主體就是一個死循環(huán)荧恍,它不停的從隊列中接受任務(wù)瓷叫,執(zhí)行任務(wù),在停止程序時送巡,我們需要一種”優(yōu)雅”的方法以關(guān)閉該線程
  • 在一些場景中摹菠,比如從第三方服務(wù)器查詢一個結(jié)果,我們希望在限定的時間內(nèi)得到結(jié)果骗爆,如果得不到次氨,我們會希望取消該任務(wù)。

總之摘投,很多情況下我們都有關(guān)閉一個線程的需求煮寡,那么如何正確的關(guān)閉一個線程就是我們要研究的事情,這個事情在上一篇文章中已經(jīng)討論過了犀呼,這里不再贅述。

廢棄的API

Thread.STOP()之類的api會造成一些不可預知的bug外臂,所以很早便Deprecated了,真要糾結(jié)為什么請看這邊文章為何不贊成使用 Thread.stop、Thread.suspend 和 Thread.resume?

線程中斷API

Thread類定義了如下關(guān)于中斷的方法:


線程對中斷的反應

RUNNABLE:線程在運行或具備運行條件只是在等待操作系統(tǒng)調(diào)度
WAITING/TIMED_WAITING:線程在等待某個條件或超時
BLOCKED:線程在等待鎖,試圖進入同步塊
NEW/TERMINATED:線程還未啟動或已結(jié)束

RUNNABLE狀態(tài)

如果線程在運行中克握,interrupt()只是會設(shè)置線程的中斷標志位,沒有任何其它作用阔馋。線程應該在運行過程中合適的位置檢查中斷標志位玛荞,比如說,如果主體代碼是一個循環(huán)呕寝,可以在循環(huán)開始處進行檢查,如下所示:

public class InterruptRunnableDemo extends Thread {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            // ... 單次循環(huán)代碼
        }
        System.out.println("done ");
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new InterruptRunnableDemo();
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

WAITING/TIMED_WAITING

線程執(zhí)行如下方法會進入WAITING狀態(tài):

public final void join() throws InterruptedException
public final void wait() throws InterruptedException

執(zhí)行如下方法會進入TIMED_WAITING狀態(tài):

public final native void wait(long timeout) throws InterruptedException;
public static native void sleep(long millis) throws InterruptedException;
public final synchronized void join(long millis) throws InterruptedException

在這些狀態(tài)時婴梧,對線程對象調(diào)用interrupt()會使得該線程拋出InterruptedException下梢,需要注意的是,拋出異常后塞蹭,中斷標志位會被清空(線程的中斷標志位會由true重置為false孽江,因為線程為了處理異常已經(jīng)重新處于就緒狀態(tài)。)番电,而不是被設(shè)置岗屏。比如說,執(zhí)行如下代碼:

Thread t = new Thread (){
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        //exception被捕獲漱办,但是為輸出為false 因為標志位會被清空
            System.out.println(isInterrupted());
        }
    }        
};
t.start();
try {
    Thread.sleep(100);
} catch (InterruptedException e) {
}
t.interrupt();//置為true

InterruptedException是一個受檢異常这刷,線程必須進行處理。我們在異常處理中介紹過娩井,處理異常的基本思路是暇屋,如果你知道怎么處理,就進行處理洞辣,如果不知道咐刨,就應該向上傳遞,通常情況下扬霜,你不應該做的是定鸟,捕獲異常然后忽略。

捕獲到InterruptedException著瓶,通常表示希望結(jié)束該線程联予,線程大概有兩種處理方式:
1、向上傳遞該異常蟹但,這使得該方法也變成了一個可中斷的方法躯泰,需要調(diào)用者進行處理
2、有些情況华糖,不能向上傳遞異常麦向,比如Thread的run方法,它的聲明是固定的客叉,不能拋出任何受檢異常诵竭,這時话告,應該捕獲異常,進行合適的清理操作卵慰,清理后沙郭,一般應該調(diào)用Thread的interrupt方法設(shè)置中斷標志位,使得其他代碼有辦法知道它發(fā)生了中斷

第一種方式的示例代碼如下:

//拋出中斷異常裳朋,由調(diào)用者捕獲
public void interruptibleMethod() throws InterruptedException{
    // ... 包含wait, join 或 sleep 方法
    Thread.sleep(1000);
}

第二種方式的示例代碼如下:

public class InterruptWaitingDemo extends Thread {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // 模擬任務(wù)代碼
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // ... 清理操作
                System.out.println(isInterrupted());//false
                // 重設(shè)中斷標志位為true
                Thread.currentThread().interrupt();
            }
        }
        System.out.println(isInterrupted());//true
    }

    public static void main(String[] args) {
        InterruptWaitingDemo thread = new InterruptWaitingDemo();
        thread.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        thread.interrupt();
    }
}

BLOCKED

如果線程在等待鎖病线,對線程對象調(diào)用interrupt()只是會設(shè)置線程的中斷標志位,線程依然會處于BLOCKED狀態(tài)鲤嫡,也就是說送挑,interrupt()并不能使一個在等待鎖的線程真正”中斷”。我們看段代碼:

public class InterruptSynchronizedDemo {
    private static Object lock = new Object();//monitor
    private static class A extends Thread {
        @Override
        public void run() {
            //等待lock鎖
            synchronized (lock) {
                    //等待標志位被置為true
                while (!Thread.currentThread().isInterrupted()) {
                }
            }
            System.out.println("exit");
        }
    }
    public static void test() throws InterruptedException {
        synchronized (lock) {//獲取鎖
            A a = new A();
            a.start();
            Thread.sleep(1000);
            //a在等待lock鎖暖眼,interrupt 無法中斷
            a.interrupt();
            //a線程加入當前線程惕耕,等待執(zhí)行完畢
            a.join();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        test();
    }
}

test方法在持有鎖lock的情況下啟動線程a,而線程a也去嘗試獲得鎖lock诫肠,所以會進入鎖等待隊列司澎,隨后test調(diào)用線程a的interrupt方法并等待線程線程a結(jié)束,線程a會結(jié)束嗎栋豫?不會挤安,interrupt方法只會設(shè)置線程的中斷標志,而并不會使它從鎖等待隊列中出來笼才。

我們稍微修改下代碼漱受,去掉test方法中的最后一行a.join,即變?yōu)椋?/p>

public static void test() throws InterruptedException {
    synchronized (lock) {
        A a = new A();
        a.start();
        Thread.sleep(1000);
        a.interrupt();
    }
    //lock鎖釋放后 A線程重隊列中出來
}

這時骡送,程序就會退出昂羡。為什么呢?因為主線程不再等待線程a結(jié)束摔踱,釋放鎖lock后虐先,線程a會獲得鎖,然后檢測到發(fā)生了中斷派敷,所以會退出蛹批。

在使用synchronized關(guān)鍵字獲取鎖的過程中不響應中斷請求,這是synchronized的局限性篮愉。如果這對程序是一個問題腐芍,應該使用顯式鎖,java中的Lock接口试躏,它支持以響應中斷的方式獲取鎖猪勇。對于Lock.lock(),可以改用Lock.lockInterruptibly()颠蕴,可被中斷的加鎖操作泣刹,它可以拋出中斷異常助析。等同于等待時間無限長的Lock.tryLock(long time, TimeUnit unit)。

NEW/TERMINATE

如果線程尚未啟動(NEW)椅您,或者已經(jīng)結(jié)束(TERMINATED)外冀,則調(diào)用interrupt()對它沒有任何效果,中斷標志位也不會被設(shè)置掀泳。比如說雪隧,以下代碼的輸出都是false。

public class InterruptNotAliveDemo {
    private static class A extends Thread {
        @Override
        public void run() {
        }
    }

    public static void test() throws InterruptedException {
        A a = new A();
        a.interrupt();
        System.out.println(a.isInterrupted());

        a.start();
        Thread.sleep(100);
        a.interrupt();
        System.out.println(a.isInterrupted());
    }

    public static void main(String[] args) throws InterruptedException {
        test();
    }
}

IO操作

如果線程在等待IO操作开伏,尤其是網(wǎng)絡(luò)IO膀跌,則會有一些特殊的處理,我們沒有介紹過網(wǎng)絡(luò)固灵,這里只是簡單介紹下。

1劫流、實現(xiàn)此InterruptibleChannel接口的通道是可中斷的:如果某個線程在可中斷通道上因調(diào)用某個阻塞的 I/O 操作(常見的操作一般有這些:serverSocketChannel. accept()巫玻、socketChannel.connect、socketChannel.open祠汇、socketChannel.read仍秤、socketChannel.write、fileChannel.read可很、fileChannel.write)而進入阻塞狀態(tài)诗力,而另一個線程又調(diào)用了該阻塞線程的 interrupt 方法,這將導致該通道被關(guān)閉我抠,并且已阻塞線程接將會收到ClosedByInterruptException苇本,并且設(shè)置已阻塞線程的中斷狀態(tài)。另外菜拓,如果已設(shè)置某個線程的中斷狀態(tài)并且它在通道上調(diào)用某個阻塞的 I/O 操作瓣窄,則該通道將關(guān)閉并且該線程立即接收到 ClosedByInterruptException;并仍然設(shè)置其中斷狀態(tài)纳鼎。
2俺夕、如果線程阻塞于Selector調(diào)用,則線程的中斷標志位會被設(shè)置贱鄙,同時劝贸,阻塞的調(diào)用會立即返回。

我們重點介紹另一種情況逗宁,InputStream的read調(diào)用映九,該操作是不可中斷的,如果流中沒有數(shù)據(jù)疙剑,read會阻塞 (但線程狀態(tài)依然是RUNNABLE)氯迂,且不響應interrupt()践叠,與synchronized類似,調(diào)用interrupt()只會設(shè)置線程的中斷標志嚼蚀,而不會真正”中斷”它禁灼,我們看段代碼

public class InterruptReadDemo {
    private static class A extends Thread {
        @Override
        public void run() {
            while(!Thread.currentThread().isInterrupted()){
                try {
                    System.out.println(System.in.read())//wait input
                } catch (IOException e) {
                    e.printStackTrace();
                }    
            }
            System.out.println("exit");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        A t = new A();
        t.start();
        Thread.sleep(100);

        t.interrupt();
    }
}

線程t啟動后調(diào)用System.in.read()從標準輸入讀入一個字符,不要輸入任何字符轿曙,我們會看到弄捕,調(diào)用interrupt()不會中斷read(),線程會一直運行导帝。

不過守谓,有一個辦法可以中斷read()調(diào)用,那就是調(diào)用流的close方法您单,我們將代碼改為:

public class InterruptReadDemo {
    private static class A extends Thread {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    System.out.println(System.in.read());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("exit");
        }
        public void cancel() {
            try {
                System.in.close();
            } catch (IOException e) {
            }
            interrupt();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        A t = new A();
        t.start();
        Thread.sleep(100);
        t.cancel();
    }
}

我們給線程定義了一個cancel方法斋荞,在該方法中,調(diào)用了流的close方法虐秦,同時調(diào)用了interrupt方法平酿,這次,程序會輸出:

-1
exit

也就是說悦陋,調(diào)用close方法后蜈彼,read方法會返回,返回值為-1俺驶,表示流結(jié)束幸逆。

如何正確地取消/關(guān)閉線程

  1. 以上,我們可以看出暮现,interrupt方法不一定會真正”中斷”線程还绘,它只是一種協(xié)作機制,如果 不明白線程在做什么送矩,不應該貿(mào)然的調(diào)用線程的interrupt方法蚕甥,以為這樣就能取消線程。

  2. 對于以線程提供服務(wù)的程序模塊而言栋荸,它應該封裝取消/關(guān)閉操作菇怀,提供單獨的取消/關(guān)閉方法給調(diào)用者,類似于InterruptReadDemo中演示的cancel方法晌块,外部調(diào)用者應該調(diào)用這些方法而不是直接調(diào)用interrupt爱沟。

  3. Java并發(fā)庫的一些代碼就提供了單獨的取消/關(guān)閉方法,比如說匆背,F(xiàn)uture接口提供了如下方法以取消任務(wù):boolean cancel(boolean mayInterruptIfRunning);

  4. 再比如呼伸,ExecutorService提供了如下兩個關(guān)閉方法:

void shutdown();
List<Runnable> shutdownNow();
  1. Future和ExecutorService的API文檔對這些方法都進行了詳細說明,這是我們應該學習的方式。

原文地址:線程的中斷(interrupt)機制

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末括享,一起剝皮案震驚了整個濱河市搂根,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌铃辖,老刑警劉巖剩愧,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異娇斩,居然都是意外死亡仁卷,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門犬第,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锦积,“玉大人,你說我怎么就攤上這事歉嗓》峤椋” “怎么了?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵鉴分,是天一觀的道長基矮。 經(jīng)常有香客問我,道長冠场,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任本砰,我火速辦了婚禮碴裙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘点额。我一直安慰自己舔株,他們只是感情好,可當我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布还棱。 她就那樣靜靜地躺著载慈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪珍手。 梳的紋絲不亂的頭發(fā)上办铡,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天,我揣著相機與錄音琳要,去河邊找鬼寡具。 笑死,一個胖子當著我的面吹牛稚补,可吹牛的內(nèi)容都是我干的童叠。 我是一名探鬼主播,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼课幕,長吁一口氣:“原來是場噩夢啊……” “哼厦坛!你這毒婦竟也來了五垮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤杜秸,失蹤者是張志新(化名)和其女友劉穎放仗,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體亩歹,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡匙监,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了小作。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片亭姥。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖顾稀,靈堂內(nèi)的尸體忽然破棺而出达罗,到底是詐尸還是另有隱情,我是刑警寧澤静秆,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布粮揉,位于F島的核電站,受9級特大地震影響抚笔,放射性物質(zhì)發(fā)生泄漏扶认。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一殊橙、第九天 我趴在偏房一處隱蔽的房頂上張望辐宾。 院中可真熱鬧,春花似錦膨蛮、人聲如沸叠纹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至踱讨,卻和暖如春惹谐,著一層夾襖步出監(jiān)牢的瞬間持偏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工豺鼻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留综液,地道東北人。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓儒飒,卻偏偏與公主長得像谬莹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內(nèi)容