5-線程的中斷

線程的中斷

使用interrupt()中斷線程

當(dāng)一個(gè)線程運(yùn)行時(shí),另一個(gè)線程可以調(diào)用對(duì)應(yīng)的Thread對(duì)象的interrupt()方法來中斷它,該方法只是在目標(biāo)線程中設(shè)置一個(gè)標(biāo)志,表示它已經(jīng)被中斷,并立即返回。這里需要注意的是,如果只是單純的調(diào)用interrupt()方法,線程并沒有實(shí)際被中斷,會(huì)繼續(xù)往下執(zhí)行。

下面一段代碼演示了休眠線程的中斷:

public class SleepInterrupt implements Runnable {
    public void run() {
        try {
            System.out.println("in run() - about to sleep for 20 seconds");
            Thread.sleep(20000);
            System.out.println("in run() - woke up");
        } catch (InterruptedException e) {
            System.out.println("in run() - interrupted while sleeping");
            // 處理完中斷異常后,返回到run()方法人口,
            // 如果沒有return,線程不會(huì)實(shí)際被中斷,它會(huì)繼續(xù)打印下面的信息
            return;
        }
        System.out.println("in run() - leaving normally");
    }

    public static void main(String[] args) {
        SleepInterrupt si = new SleepInterrupt();
        Thread t = new Thread(si);
        t.start();
        // 主線程休眠2秒,從而確保剛才啟動(dòng)的線程有機(jī)會(huì)執(zhí)行一段時(shí)間
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("in main() - interrupting other thread");
        // 中斷線程t
        t.interrupt();
        System.out.println("in main() - leaving");
    }
}

運(yùn)行結(jié)果如下:

image.png

主線程啟動(dòng)新線程后,自身休眠2秒鐘,允許新線程獲得運(yùn)行時(shí)間。新線程打印信息“about to sleep for 20 seconds”后,繼而休眠20秒鐘,大約2秒鐘后,main線程通知新線程中斷,那么新線程的20秒的休眠將被打斷,從而拋出InterruptedException異常,執(zhí)行跳轉(zhuǎn)到catch塊,打印出“interrupted while sleeping”信息,并立即從run()方法返回,然后消亡,而不會(huì)打印出catch塊后面的“l(fā)eaving normally”信息。

請(qǐng)注意:由于不確定的線程規(guī)劃,上圖運(yùn)行結(jié)果的后兩行可能順序相反,這取決于主線程和新線程哪個(gè)先消亡。但前兩行信息的順序必定如上圖所示。

另外,如果將catch塊中的return語句注釋掉,則線程在拋出異常后,會(huì)繼續(xù)往下執(zhí)行,而不會(huì)被中斷,從而會(huì)打印出”leaving normally“信息。

待決中斷

在上面的例子中,sleep()方法的實(shí)現(xiàn)檢查到休眠線程被中斷,它會(huì)相當(dāng)友好地終止線程,并拋出InterruptedException異常。另外一種情況,如果線程在調(diào)用sleep()方法前被中斷,那么該中斷稱為待決中斷,它會(huì)在剛調(diào)用sleep()方法時(shí),立即拋出InterruptedException異常。

下面的代碼演示了待決中斷:

public class PendingInterrupt {
    public static void main(String[] args) {
        Thread.currentThread().interrupt();
        try {
            Thread.sleep(2000);
            System.out.println("was NOT interrupted");
        } catch (InterruptedException x) {
            System.out.println("was interrupted");
        }
    }
}
image.png

這種模式下,main線程中斷它自身。除了將中斷標(biāo)志(它是Thread的內(nèi)部標(biāo)志)設(shè)置為true外,沒有其他任何影響。線程被中斷了,但main線程仍然運(yùn)行,main線程進(jìn)入try塊,一旦調(diào)用sleep()方法,它就會(huì)注意到待決中斷的存在,并拋出InterruptedException。于是執(zhí)行跳轉(zhuǎn)到catch塊,并打印出線程被中斷的信息。

使用isInterrupted()方法判斷中斷狀態(tài)

可以在Thread對(duì)象上調(diào)用isInterrupted()方法來檢查任何線程的中斷狀態(tài)。這里需要注意:線程一旦被中斷,isInterrupted()方法便會(huì)返回true,而一旦sleep()方法拋出異常,它將清空中斷標(biāo)志,此時(shí)isInterrupted()方法將返回false。

下面的代碼演示了isInterrupted()方法的使用:

public class InterruptCheck {
    public static void main(String[] args) {
        Thread t = Thread.currentThread();
        System.out.println("Point A: t.isInterrupted()=" + t.isInterrupted());
        // 待決中斷,中斷自身  
        t.interrupt();
        System.out.println("Point B: t.isInterrupted()=" + t.isInterrupted());
        System.out.println("Point C: t.isInterrupted()=" + t.isInterrupted());

        try {
            Thread.sleep(2000);
            System.out.println("was NOT interrupted");
        } catch (InterruptedException x) {
            System.out.println("was interrupted");
        }
        // 拋出異常后,會(huì)清除中斷標(biāo)志,這里會(huì)返回false  
        System.out.println("Point D: t.isInterrupted()=" + t.isInterrupted());
    }
}

執(zhí)行結(jié)果如下:

image.png

使用Thread.interrupted()方法判斷中斷狀態(tài)

可以使用靜態(tài)的Thread.interrupted()方法來檢查當(dāng)前線程的中斷狀態(tài)(并隱式重置為false)。又由于它是靜態(tài)方法,因此不能在特定的線程上使用,而只能報(bào)告調(diào)用它的線程的中斷狀態(tài),如果線程被中斷,而且中斷狀態(tài)尚不清楚,那么,這個(gè)方法返回true。與isInterrupted()不同,它將自動(dòng)重置中斷狀態(tài)為false,第二次調(diào)用Thread.interrupted()方法,總是返回false,除非中斷了線程。

如下代碼演示了Thread.interrupted()方法的使用:

public class InterruptReset {
    public static void main(String[] args) {
        System.out.println("Point X: Thread.interrupted()="
                + Thread.interrupted());
        Thread.currentThread().interrupt();
        System.out.println("Point Y: Thread.interrupted()="
                + Thread.interrupted());
        System.out.println("Point Z: Thread.interrupted()="
                + Thread.interrupted());
    }
}

運(yùn)行結(jié)果如下:

image.png

從結(jié)果中可以看出,當(dāng)前線程中斷自身后,在Y點(diǎn),中斷狀態(tài)為true,并由Thread.interrupted()自動(dòng)重置為false,那么下次調(diào)用該方法得到的結(jié)果便是false。

線程中斷(摘自 Java并發(fā)編程實(shí)戰(zhàn) )

線程中斷是一種協(xié)作機(jī)制,線程可以通過這種機(jī)制來通知另一個(gè)線程,告訴它在合適的或者可能的情況下停止當(dāng)前工作,并轉(zhuǎn)而執(zhí)行其他的工作。

每個(gè)線程都有一個(gè)boolean類型的中斷狀態(tài)。當(dāng)中斷線程時(shí),這個(gè)線程的中斷狀態(tài)將被設(shè)置為true。在Thread中包含了中斷線程以及查詢線程中斷狀態(tài)的方法,如以下代碼所示。interrupt方法能中斷目標(biāo)線程,而isInterrupted方法能返回目標(biāo)線程的中斷狀態(tài)。靜態(tài)的interrupted方法將清除當(dāng)前線程的中斷狀態(tài),并返回它之前的值,這也是清除中斷狀態(tài)的唯一方法。

public class Thread {
    public void interrupt() {...}
    public boolean isInterrupted() {...}
    public static boolean interrupted() {...}
    ...
}

阻塞庫(kù)方法,例如Thread.sleep、BlockingQueue.putObject.wait等,都會(huì)檢查線程何時(shí)中斷,并且在發(fā)現(xiàn)中斷時(shí)提前返回。它們?cè)陧憫?yīng)中斷時(shí)執(zhí)行的操作包括:清除中斷狀態(tài),拋出InterruptedException,表示阻塞操作由于中斷而提前結(jié)束。JVM并不能保證阻塞方法檢測(cè)到中斷的速度,但在實(shí)際情況中響應(yīng)速度還是非??斓摹?/p>

當(dāng)線程在非阻塞狀態(tài)下中斷時(shí),它的中斷狀態(tài)將被設(shè)置,然后根據(jù)將被取消的操作來檢查中斷狀態(tài)以判斷發(fā)生了中斷。通過這樣的方法,中斷操作將變得“有黏性”——如果不觸發(fā)InterruptedException,那么中斷狀態(tài)將一直保持,直到明確地清除中斷狀態(tài)。

調(diào)用interrupt 并不意味著立即停止目標(biāo)線程正在進(jìn)行的工作,而只是傳遞了請(qǐng)求中斷的消息。

對(duì)中斷操作的正確理解是:它并不會(huì)真正地中斷一個(gè)正在運(yùn)行的線程,而只是發(fā)出中斷請(qǐng)求,然后由線程在下一個(gè)合適的時(shí)刻中斷自己。(這些時(shí)刻也被稱為取消點(diǎn))。有些方法,例如wai、sleepjoin等,將嚴(yán)格地處理這種請(qǐng)求,當(dāng)它們收到中斷請(qǐng)求或者在開始執(zhí)行時(shí)發(fā)現(xiàn)某個(gè)已被設(shè)置好的中斷狀態(tài)時(shí),將拋出一個(gè)異常。設(shè)計(jì)良好的方法可以完全忽略這種請(qǐng)求,只要它們能使調(diào)用代碼對(duì)中斷請(qǐng)求進(jìn)行某種處理。設(shè)計(jì)糟糕的方法可能會(huì)屏蔽中斷請(qǐng)求,從而導(dǎo)致調(diào)用棧中的其它代碼無法對(duì)中斷請(qǐng)求作出響應(yīng)。

在使用靜態(tài)的interrupted時(shí)應(yīng)該小心,因?yàn)樗鼤?huì)清除當(dāng)前線程的中斷狀態(tài)。如果在調(diào)用interrupted時(shí)返回了true,那么除非你想屏蔽這個(gè)中斷,否則必須對(duì)它進(jìn)行處理——可以拋出InterruptedException,或者通過再次調(diào)用interrupt來恢復(fù)中斷狀態(tài)。

如何處理中斷異常(InterruptedException)

中斷是一種協(xié)作機(jī)制。一個(gè)線程不能強(qiáng)制其它線程停止正在執(zhí)行的操作而去執(zhí)行其它的操作。當(dāng)線程A中斷B時(shí),A僅僅是要求B在執(zhí)行到某個(gè)可以暫停的地方停止正在執(zhí)行的操作——前提是如果線程B愿意停止下來。雖然在API或者語言規(guī)范中并沒有為中斷定義任何特定應(yīng)用級(jí)別的語義,但最常使用的中斷的情況就是取消某個(gè)操作。方法對(duì)中斷請(qǐng)求的響應(yīng)度越高,就越容易及時(shí)取消那些執(zhí)行時(shí)間很長(zhǎng)的操作。

當(dāng)在代碼中調(diào)用了一個(gè)將拋出InterruptedException異常的方法時(shí),你自己的方法也就變成了一個(gè)阻塞方法,并且必須要處理對(duì)中斷的響應(yīng)。有兩種實(shí)用策略可用于處理InterruptedException

  • 傳遞InterruptedException

避開這個(gè)異常通常是最明智的策略——只需把InterruptedException傳遞給方法的調(diào)用者。傳遞InterruptedException的方法包括,根本不捕獲該異常,或者捕獲該異常,然后在執(zhí)行某種簡(jiǎn)單的清理工作后再次拋出這個(gè)異常。

// 將InterruptedException傳遞給調(diào)用者
BlockingQueue<Task> queue;
...
public Task getNextTask() throws InterruptedException {
    return queue.take();
}
  • 恢復(fù)中斷

有時(shí)候不能拋出InterruptedException,例如當(dāng)代碼是Runnable的一部分時(shí),在這些情況下,必須捕獲InterruptedException,并通過調(diào)用當(dāng)前線程上的interrupt方法恢復(fù)中斷狀態(tài),這樣在調(diào)用棧中更高層的代碼將看到引發(fā)了一個(gè)中斷。你不能屏蔽InterruptedException,例如在catch塊中捕獲到異常卻不做任何處理,除非在你的代碼中實(shí)現(xiàn)了線程的中斷策略。

// 恢復(fù)中斷狀態(tài)以避免屏蔽中斷
public class TaskRunnable implements Runnable {
    BlockingQueue<Task> queue;
    ...
    public void run() {
        try {
            processTask(queue.take());
        } catch(InterruptedException e) {
            // 恢復(fù)被中斷的狀態(tài)
            Thread.currentThread().interrupt();
        }
    }
}

處理不可中斷的阻塞

在Java庫(kù)中,許多可阻塞的方法都是通過提前返回或者拋出InterruptedException來響應(yīng)中斷請(qǐng)求的,從而使開發(fā)人員更容易構(gòu)建出能響應(yīng)取消請(qǐng)求的任務(wù)。然而,并非所有的可阻塞方法或者阻塞機(jī)制都能響應(yīng)中斷;如果一個(gè)線程由于執(zhí)行同步的Socket I/O或者等待獲得內(nèi)置鎖而阻塞,那么中斷請(qǐng)求只能設(shè)置線程的中斷狀態(tài),除此之外沒有其他任何作用。

  • Java.io包中的同步Socket I/O

在服務(wù)器應(yīng)用程序中,最常見的阻塞I/O形式就是對(duì)套接字進(jìn)行讀取和寫入。雖然InputStreamOutputStream中的readwrite等方法都不會(huì)響應(yīng)中斷,但通過關(guān)閉底層的套接字,可以使得由于執(zhí)行readwrite等方法而被阻塞的線程拋出一個(gè)SocketException

  • Selector的異步I/O

如果一個(gè)線程在調(diào)用Selector.select方法(在java.nio.channels中)時(shí)阻塞了,那么調(diào)用closewakeup方法會(huì)使線程拋出ClosedSelectorException并提前返回。

  • 獲取某個(gè)鎖

如果一個(gè)線程由于等待某個(gè)內(nèi)置鎖而阻塞,那么將無法響應(yīng)中斷,因?yàn)榫€程認(rèn)為它肯定會(huì)獲得鎖,所以將不會(huì)理會(huì)中斷請(qǐng)求。但是,在Lock類中提供了lockInterruptibly方法,該方法允許在等待一個(gè)鎖的同時(shí)仍能響應(yīng)中斷。

處理不可中斷的阻塞可參考以下代碼:

// 通過改寫interrupt方法將非標(biāo)準(zhǔn)的取消操作封裝在Thread中
public class ReaderThread extends Thread {
    private final Socket socket;
    private final InputStream in;
        
    public ReaderThread(Socket socket) throws IOException {
        this.socket = socket;
        this.in = socket.getInputStream();
    }

    public void interrupt() {
        try {
            socket.close();
        } catch(IOException ignored) {
            
        } finally {
            super.interrupt();
        }
    }

    public void run() {
        try {
            byte[] buf = new byte[BUFSZ];
            while(true) {
                int count = in.read(buf);
                if (count < 0)
                    break;
                else if (count > 0)
                    processBuffer(buf, count);
            }
        } catch (IOException e) {
            // 允許線程退出
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容