1. 程式人生 > >Java多執行緒程式設計(6)--執行緒間通訊(下)

Java多執行緒程式設計(6)--執行緒間通訊(下)

  因為本文的內容大部分是以生產者/消費者模式來進行講解和舉例的,所以在開始學習本文介紹的幾種執行緒間的通訊方式之前,我們先來熟悉一下生產者/消費者模式。   在實際的軟體開發過程中,經常會碰到如下場景:某個模組負責產生資料(可能是訊息、檔案、任務等),這些資料由另一個模組來負責處理。產生資料的模組,就形象地被稱為生產者;而處理資料的模組,就被稱為消費者。   單單抽象出生產者和消費者,還稱不上是生產者/消費者模式。該模式還需要有一個緩衝區處於生產者和消費者之間來作為一箇中介。生產者把資料放入緩衝區,而消費者從緩衝區取出資料。因此,生產者/消費者模式大概的結構如下圖: ![](https://maconn.oss-cn-beijing.aliyuncs.com/19/12/20191201170923.png)   我們可以使用不同的執行緒來模擬生產者和消費者。對應的,生產資料的執行緒就被稱為是生產者執行緒,而消費資料的執行緒就被稱為是消費者。而緩衝區則可以選用那些執行緒安全的資料結構來模擬,因此,緩衝區在這裡就起到了執行緒間通訊工具的作用。本文將會通過生產者/消費者模式作為例子來介紹幾種執行緒間的通訊方式。 ## 一.阻塞佇列 ### 1.BlockingQueue介面   如何選擇合適的資料結構來作為緩衝區呢?首先我們來分析一下我們的需求。首先,消費者應該是要按照生產者生產的順序來消費資料的,那麼我們腦海中浮現的一定是具有先進先出特性的隊列了。其次,既然是在多執行緒之間進行傳遞,那麼這個類一定是執行緒安全的。因此,緩衝區應該使用執行緒安全的佇列。我們首先想到的應該是ConcurrentLinkedQueue,它是使用連結串列實現的執行緒安全的佇列。但是,這個類是非阻塞的,這意味著當生產者向緩衝區中放入資料時,緩衝區是否已滿時需要生產者自己去判斷的;同理,當消費者去消費緩衝區中的資料時,緩衝區是否為空也是需要自己去判斷的。由於這個類是非阻塞的,因此我們只能在執行緒中不斷的去輪詢緩衝區,這顯然不是多執行緒程式設計該有的實現方式。   那麼,有沒有可以阻塞執行緒的佇列呢?答案是肯定的。java.util.concurrent包中的BlockingQueue介面定義了阻塞佇列的行為。當阻塞佇列中沒有資料的時候,消費者端的執行緒會被掛起直到有資料被放入佇列;當阻塞佇列中填滿資料的時候,生產者端的執行緒會被掛起直到佇列中有空的位置。   下面來介紹BlockingQueue介面中定義的方法。BlockingQueue介面時Queue介面的子介面,因此它繼承了Queue介面中所有的方法,由於這些方法都比較簡單,因此不再贅述。這裡只介紹BlockingQueue介面中新增的方法。 #### (1)放入資料 - **void put​(E e) throws InterruptedException** 將指定元素e放入佇列。如果佇列沒有空間,則當前執行緒會阻塞直到佇列有空間。 - **boolean offer​(E e, long timeout, TimeUnit unit) throws InterruptedException** 將指定元素e放入佇列。如果佇列沒有空間,則當前執行緒會阻塞直到佇列有空間或等待超時。 #### (2)取出資料 - **E take() throws InterruptedException** 從佇列中取出元素。如果佇列中沒有元素,則當前執行緒會阻塞直到佇列中有元素。 - **E poll​(long timeout, TimeUnit unit) throws InterruptedException** 從佇列中取出元素。如果佇列中沒有元素,則當前執行緒會阻塞直到佇列中有元素或等待超時。 - **int drainTo​(Collection c)** 一次性從佇列中取出所有可用的資料物件放在指定的集合中。 - **int drainTo​(Collection c, int maxElements)** 同上,maxElements可以限制最多獲取的元素個數。 ### 2.BlockingQueue介面的實現類   java.util.concurrent包為BlockingQueue介面提供了7個實現類。 #### (1)ArrayBlockingQueue   ArrayBlockingQueue是基於陣列實現的有界的阻塞佇列,其內部維護了一個定長陣列來存放佇列中的資料物件。由於其是有界的,因此在構造ArrayBlockingQueue例項時,必須提供佇列的容量。這是一個非常常用的阻塞佇列,除了一個定長陣列外,ArrayBlockingQueue內部還維護了兩個整形變數,分別標識著佇列的頭部和尾部在陣列中的位置。 >   阻塞佇列按照其儲存空間的容量是否受限制來劃分,可以分為有界佇列和無界佇列。有界佇列的儲存容量限制是在構造例項的時候指定的,而無界佇列實際上也有儲存容量限制,其預設的最大儲存容量為Integer.MAX_VALUE(即231
-1)個元素。然而實際情況是,無界佇列往往會在還沒到達儲存容量限制時就已經造成了OutOfMemoryError。   ArrayBlockingQueue在寫入資料和獲取資料時,使用的是同一個鎖物件,這就意味著兩者無法達到真正的並行。其實按照實現原理來分析,ArrayBlockingQueue完全在兩種操作上使用不同的鎖,從而實現生產者和消費者操作的完全並行。Doug Lea之所以沒這樣去做,也許是因為ArrayBlockingQueue的資料寫入和獲取操作已經足夠輕巧,以至於引入獨立的鎖機制,除了給程式碼帶來額外的複雜性外,在效能上並不會有太大的提升。   此外,在建立ArrayBlockingQueue例項時,我們還可以指定內部的鎖是否採用公平鎖,預設情況下采用非公平鎖。 #### (2)LinkedBlockingQueue   LinkedBlockingQueue是基於連結串列實現的阻塞佇列,其既可以是有界的,也可以是無界的。如果在構造LinkedBlockingQueue例項時沒有提供佇列的容量,則會構造出一個無界的佇列,反之則會構造出一個有界的佇列。   LinkedBlockingQueue也是一個非常常用的阻塞佇列,其內部維護了一個連結串列,對於資料的寫入操作是在連結串列頭部進行的,而對於資料的獲取操作是在連結串列尾部進行的。LinkedBlockingQueue內部對於資料的寫入和讀取採用了兩個鎖來控制,即putLock和takeLock,它們都是非公平鎖。兩種操作對應了兩把鎖意味著在高併發的情況下生產者和消費者可以並行地操作佇列中的資料,這可以有效地提高整個佇列的併發效能。但是,相較於ArrayBlockingQueue,LinkedBlockingQueue在寫入和讀取資料時,需要動態地建立和刪除連結串列節點,在高併發和資料量大的時候,GC壓力很大。   ArrayBlockingQueue和LinkedBlockingQueue是兩個最普通也是最常用的阻塞佇列,一般情況下,在處理多執行緒間的生產者消費者問題時,使用這兩個類足以解決大部分問題。 #### (3)DelayQueue   DelayQueue是一個存放延時元素的無界阻塞佇列,它對佇列中的元素做出了限制,即E extends Delayed,這意味著佇列中的元素必須實現Delayed介面。Delayed介面用於標記具有延時功能的物件,即只有在給定的延遲時間結束之後才能對該物件進行操作。Delayed介面中只定義了一個方法getDelay​(TimeUnit unit),但是由於它是Comparable介面的子介面,因此它還繼承了compareTo方法,這個方法在實現時需要根據getDelay的結果來進行排序。   DelayQueue內部維護了一個優先順序佇列,即PriorityQueue,該佇列是以延時結束的時間做為優先順序來存放元素的,延時結束時間越早,優先順序越高。當試圖從延時佇列中取出元素時,會先從優先順序佇列中取出優先順序最高的元素,若該元素延時時間已經結束,則直接返回;否則將會阻塞當前執行緒直到延時結束。向佇列中放入元素時,除了獲取操作優先順序佇列的鎖之外沒有其他限制。該佇列的讀取和寫入使用的是同一把鎖(非公平鎖),因此該佇列的消費者和生產者無法並行操作。 #### (4)PriorityBlockingQueue   PriorityBlockingQueue很好理解,可以將其看作執行緒安全的、具有阻塞功能的無界優先順序佇列。PriorityBlockingQueue的put和take操作都加了鎖,並且它們使用的是同一把非公平鎖,這意味著該佇列上的消費者和生產者無法並行操作。由於該佇列是無界的,因此該佇列不會阻塞生產者,但是當佇列中沒有元素的時候會阻塞消費者。雖然該佇列是無界的,但是它仍然提供了可以指定佇列初始化大小的構造方法,這是因為該佇列會在佇列已滿的情況下進行自動擴容。 #### (5)SynchronousQueue   SynchronousQueue是一種較為特殊的阻塞佇列,其內部並沒有儲存佇列元素的空間。當生產者執行緒執行put操作時,如果沒有消費者執行緒在執行take操作,那麼該生產者執行緒會被阻塞;當消費者執行緒在執行take操作時,如果沒有生產者執行緒在執行put操作,那麼該消費者執行緒也會被阻塞。   SynchronousQueue類提供了兩個構造方法,分別是SynchronousQueue()和SynchronousQueue(boolean fair)。第一種構造器預設採用了非公平策略(實際上是LIFO),第二種構造器則可以指定佇列採用非公平策略還是公平策略。   此外,由於SynchronousQueue本身並不儲存元素,因此該佇列對於Queue介面中定義的大部分方法都具有固定的返回值,例如peek()總是返回null,size()總是返回0等。 #### (6)LinkedTransferQueue   LinkedTransferQueue是一種用連結串列實現的無界阻塞佇列,它實現了TransferQueue介面,而TransferQueue介面是BlockingQueue介面的子介面,因此它也是阻塞佇列的一種。   下面是TransferQueue介面定義的方法: ![](https://maconn.oss-cn-beijing.aliyuncs.com/19/12/20191209212142.png)   除了這幾個方法外,該佇列其他方法的行為和LinkedBlockingQueue類似,這裡不再過多贅述。 #### (7)LinkedBlockingDeque   顧名思義,這個類是用連結串列實現的阻塞雙端佇列,和LinkedBlockingQueue類似,它既可以是有界的,也可以是無界的。實際上,除了具有雙端佇列的特性外,該類與LinkedBlockingQueue十分相似,可以參照上面對LinkedBlockingQueue的介紹來理解它,這裡不再詳細介紹。 ## 二.訊號量Semaphore   Semaphore類是一個計數訊號量。為了便於討論,我們把程式碼所訪問的特定資源或者執行特定操作的機會同意看作是一種資源,可以將其稱之為虛擬資源。Semaphore相當於虛擬資源訪問許可管理器,它可以用來控制同一時間內對虛擬資源的訪問次數。為了對虛擬資源的訪問進行流量控制,我們必須使相應程式碼只有在獲得許可的情況下才能夠訪問這些資源。基於這種思想,在訪問虛擬資源前應該先申請許可,在訪問後應該釋放許可。   Semaphore的acquire和release方法分別用於申請和釋放許可。如果當前可用的許可數等於0或小於0(在構造Semaphore例項的時候可以指定許可數為0或負數),那麼acquire方法會使執行執行緒暫停。Semaphore內部維護了一個佇列來儲存這些被暫停的執行緒,預設情況下,Semaphore使用非公平策略,當然也可以在構造方法中顯式指定Semaphore例項使用公平策略還是非公平策略。   下面是Semaphore類提供的所有方法: ![](https://maconn.oss-cn-beijing.aliyuncs.com/19/12/20191212091325.png) ## 三.管道流   Java語言提供了各種各樣的輸入/輸出流,使我們能夠很方便地對資料進行操作,其中管道流是一種特殊的流,用於在不同的執行緒間直接傳送資料。一個執行緒傳送資料到管道,另一個執行緒從管道中讀取資料。通過使用管道,可以實現不同執行緒間的通訊,而無需藉助臨時檔案等資料中介。   和其他流類似,管道流也分為位元組流和字元流,其中位元組流對應的輸入流和輸出流分別是PipedInputStream和PipedOutputStream,字元流對應的輸入流和輸出流分別是PipedReader和PipedWriter。   管道流實際上是使用一個迴圈緩衝陣列來實現的,輸入流從這個陣列中讀資料,輸出流向這個陣列中寫資料。當緩衝區滿時,輸出流所在的執行緒將會阻塞,當緩衝區空時,輸入流所在的執行緒將會阻塞。這個陣列位於輸入流內部,預設大小為1024,也可以通過管道輸入流的構造方法來指定緩衝陣列大小。   管道輸入流和輸出流在使用之前必須先建立連線,可以通過輸入流或輸出流的構造方法或connect方法來使兩個流建立連線。假設in是執行緒A的輸入流,out是執行緒B的輸出流,那麼可以通過以下幾種方法來建立連線: ```java PipedInputStream in = new PipedInputStream(out); //方法1 PipedInputStream in = new PipedInputStream(); //方法2 in.connect(out); PipedOutputStream out = new PipedOutputStream(in); //方法3 PipedOutputStream out = new PipedOutputStream(); //方法4 out.connect(in); ```   不要在同一個執行緒中同時使用管道輸入流和管道輸出流,這樣有可能會引起死鎖。因為當緩衝區滿時,如果繼續向輸出流中寫入資料,則會阻塞當前執行緒,從而造成從輸入流中讀取資料的程式碼永遠不會被執行到,造成死鎖;緩衝區空時,如果繼續從輸出流中讀取資料,也會阻塞當前執行緒,從而造成向輸出流中寫入資料的程式碼永遠不會被執行到,造成死鎖。   下面是使用管道字元流編寫的一個demo: ```java import java.io.IOException; import java.io.PipedReader; import java.io.PipedWriter; public class PipedStreamDemo { private static final String content = "Hello world"; public static void main(String[] args) { try { PipedReader reader = new PipedReader(); PipedWriter writer = new PipedWriter(reader); Receiver receiver = new Receiver(reader); Sender sender = new Sender(writer); receiver.start(); sender.start(); } catch (IOException e) { e.printStackTrace(); } } private static class Sender extends Thread { private PipedWriter writer; public Sender(PipedWriter writer) { this.writer = writer; } @Override public void run() { try { System.out.println("Send : " + content); char[] chars = content.toCharArray(); writer.write(chars, 0, chars.length); writer.close(); } catch (IOException e) { e.printStackTrace(); } } } private static class Receiver extends Thread { private PipedReader reader; public Receiver(PipedReader reader) { this.reader = reader; } @Override public void run() { try { int ch; while ((ch = reader.read()) != -1) { System.out.println("Received : " + (char) ch); } } catch (IOException e) { e.printStackTrace(); } } } } ```   該程式輸出如下: ``` Send : Hello world Received : H Received : e Received : l Received : l Received : o Received : Received : w Received : o Received : r Received : l Received : d ``` ## 四.交換器Exchanger   Exchanger<V>是一個用於在兩個執行緒之間交換資料的工具類。兩個執行緒可以通過同一個Exchanger例項的exchange方法來交換資料。當一個執行緒先執行exchange方法時,它會被阻塞並等待另一個執行緒的到來;當另一個執行緒也執行exchange方法時,前一個執行緒會被喚醒,兩個執行緒完成資料交換並繼續執行。   Exchanger提供了兩個exchange方法: ![](https://maconn.oss-cn-beijing.aliyuncs.com/20/01/20200104105716.png)   下面是一個使用Exchanger的例子: ```java import java.util.concurrent.Exchanger; public class ExchangerDemo { public static void main(String[] args) { Exchanger exchanger = new Exchanger<>(); new Buyer(exchanger).start(); new Seller(exchanger).start(); } private static class Buyer extends Thread { private Exchanger exchanger; Buyer(Exchanger exchanger) { this.exchanger = exchanger; } @Override public void run() { try { String money = "10元"; Thread.sleep(2000); System.out.println("買家:拿出" + money); String good = exchanger.exchange(money); System.out.println("買家:得到" + good); } catch (InterruptedException e) { e.printStackTrace(); } } } private static class Seller extends Thread { private Exchanger exchanger; Seller(Exchanger exchanger) { this.exchanger = exchanger; } @Override public void run() { try { String good = "10斤大白菜"; System.out.println("賣家:拿出" + good); System.out.println("賣家:等待買家..."); String money = exchanger.exchange(good); System.out.println("賣家:得到" + money); } catch (InterruptedException e) { e.printStackTrace(); } } } } ```   該程式的輸出如下: ``` 賣家:拿出10斤大白菜 賣家:等待買家... 買家:拿出10元 買家:得到10斤大白菜 賣家:得到10元 ``` ## 五.執行緒中斷機制   有時候我們需要停止一個執行緒,例如一個下載執行緒,該執行緒在沒有下載成功之前不會退出,若此時使用者覺得下載速度慢,不想下載而點選了取消按鈕,此時我們應該停止這個下載執行緒並釋放資源。但是,由於Thread.stop、Thread.suspend和Thread.resume過於暴力而被廢棄,那麼我們應該如何優雅地停止一個執行緒呢?   Java為我們提供了執行緒中斷機制。中斷機制的思想是:一個執行緒不應該由其他執行緒來強制中斷,而是應該由執行緒自己自行判斷。中斷可以看作是由一個執行緒傳送給另外一個執行緒的一種指示,該指示用於表示發起執行緒希望目標執行緒停止其正在執行的操作。但是,中斷一個執行緒並不代表馬上停止該執行緒,而只是通知該執行緒應該中斷了,是否應該中斷以及如何響應中斷則應該交給該執行緒來自行處理。 >  在Java的API或語言規範中,並沒有將中斷與任何取消語義關聯起來。但實際上,如果在取消之外的其他操作中使用中斷,那麼都是不合適的,並且很難支援起更大的應用。 ### 1.API   實際上,每個執行緒內部都有一個boolean型別的中斷標記,當中斷一個執行緒時,該執行緒內部的中斷標記將會被設定為true。以下是Thread類中與中斷有關的三個方法: ```java void interrupt() boolean isInterrupted() static boolean interrupted() ```   下面將分別對這三個方法進行介紹。 #### (1)interrupt   呼叫一個執行緒的interrupt方法會將該執行緒的中斷標記設定為true。 1. 如果該執行緒由於呼叫Object.wait()、Object.wait(long)、Object.wait(long, int)、Thread.join()、Thread.join(long)、Thread.join(long, int)、Thread.sleep()或Thread.sleep(long, int)而進入等待狀態,該執行緒的中斷標記將會被清除並收到一個InterruptedException。 2. 如果該執行緒阻塞在一個基於InterruptibleChannel的I/O操作上,這個channel將會被關閉並收到一個ClosedByInterruptException。 3. 如果該執行緒被阻塞在一個Selector裡,則該執行緒會馬上從選擇操作中返回,返回值可能是非0值,就好像Selector的wakeup方法被呼叫過一樣。 4. 如果以上條件均不成立,那麼該執行緒僅僅只是中斷標記被設定為true,並不會表現出其他行為。   上面的2、3兩個條件與NIO有關,這裡只是順便提到而已。後續會推出關於NIO的系列教程。 #### (2)isInterrupted   該方法較為簡單,只是返回該執行緒的中斷標記,並不會影響該中斷標記和產生其他行為。 #### (3)interrupted   該方法是一個靜態方法,也會返回該執行緒的中斷標記。但是該方法與isInterrupted最大的區別在於該方法會清除執行緒的中斷狀態。例如,如果當前執行緒已經被中斷,那麼呼叫interrupted方法將會返回true並同時清除中斷狀態;如果當前執行緒未被中斷,則會返回false。這個方法的好處在於返回了執行緒中斷標記的同時還清除了中斷標記。 ### 2.執行緒在不同狀態下對中斷的反應   執行緒一共有6種狀態,分別是NEW、RUNNABLE、WAITING、TIMED_WAITING、BLOCKED和TERMINATED。在不同狀態下,執行緒可能會對中斷產生不同的反應。 #### (1)NEW/TERMINATED   由於處於NEW狀態的執行緒還沒有啟動,而處於TERMINATED狀態的執行緒已經終止,Java認為對處於這兩種狀態下的執行緒進行中斷毫無意義,所以並不會將執行緒的中斷標識設定為true,也不會產生其他的行為。 ```java public class NewAndTerminatedDemo { public static void main(String[] args) { Thread thread = new Thread(); System.out.println(thread.getState()); thread.interrupt(); System.out.println(thread.isInterrupted()); thread.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(thread.getState()); thread.interrupt(); System.out.println(thread.isInterrupted()); } } ```   上面的例子輸出如下: ``` NEW false TERMINATED false ``` #### (2)RUNNABLE   處於RUNNABLE狀態下的執行緒被中斷後,除了中斷標記被設定為true外不會產生其他行為,下面我們來做個實驗: ```java public class RunnableDemo { public static void main(String[] args) { TimeWasteThread timeWasteThread = new TimeWasteThread(); timeWasteThread.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } timeWasteThread.interrupt(); System.out.println("The interrupt flag of timeWasteThread is " + timeWasteThread.isInterrupted()); } private static class TimeWasteThread extends Thread { @Override public void run() { while (true) { System.out.println("The 40th fibonacci number is " + fibonacci(40)); } } private int fibonacci(int n) { if (n == 1 || n == 2) { return 1; } return fibonacci(n - 1) + fibonacci(n - 2); } } } ```   在上面的例子中,TimeWasteThread內部執行了一個非常耗時的操作——計算第40個斐波那契數(這種寫法在每次計算時都需要重新遞迴,因此非常耗時),這樣做的目的是使它一直處於RUNNABLE狀態,我們既不希望它太快結束,也不希望使用sleep方法,因為這是下一小節要討論的內容。主執行緒在啟動TimeWasteThread兩秒後中斷了該執行緒。該程式的輸出如下: ``` The 40th fibonacci number is 102334155 The 40th fibonacci number is 102334155 The 40th fibonacci number is 102334155 The 40th fibonacci number is 102334155 The 40th fibonacci number is 102334155 The 40th fibonacci number is 102334155 The interrupt flag of timeWasteThread is true The 40th fibonacci number is 102334155 The 40th fibonacci number is 102334155 The 40th fibonacci number is 102334155 The 40th fibonacci number is 102334155 ... ```   可以看到,在主執行緒對TimeWasteThread發出了中斷訊號後,TimeWasteThread的中斷標記確實變成了true,可以程式仍然在繼續執行,沒有受到任何影響。雖然主執行緒已經呼叫了TimeWasteThread的interrupt方法,可該執行緒並沒有中斷執行。既然如此,那我要這中斷機制有何用?   我們在上面提到過,中斷機制的思想是一個執行緒是否中斷應該由該執行緒來判斷,而不應該由其他執行緒來控制。因此,我們可以線上程內部來判斷當前執行緒是否需要被中斷,然後做出相應的決策。   基於這種思想,我們將TimeWasteThread的run方法修改如下: ```java @Override public void run() { while (!Thread.interrupted()) { System.out.println("The 40th fibonacci number is " + fibonacci(40)); } } ```   重新執行該程式,輸出如下: ``` The 40th fibonacci number is 102334155 The 40th fibonacci number is 102334155 The 40th fibonacci number is 102334155 The 40th fibonacci number is 102334155 The 40th fibonacci number is 102334155 The 40th fibonacci number is 102334155 The interrupt flag of timeWasteThread is true The 40th fibonacci number is 102334155 ```   可以看到,TimeWasteThread在收到中斷訊號後很快就停了下來,達到了中斷的目的。 #### (3)WAITING/TIMED_WAITING   這兩種狀態本質上可以看作是等待狀態,只不過一個是無限期等待,而另一個是有時間限制的等待,因此放在一起討論。   下面的例子中,我們讓SleepingThread進入TIMED_WAITING狀態後將其中斷: ```java public class WaitingDemo { public static void main(String[] args) { Thread sleepingThread = new SleepingThread(); sleepingThread.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } sleepingThread.interrupt(); } private static class SleepingThread extends Thread { @Override public void run() { try { Thread.sleep(5000); } catch (InterruptedException e) { System.out.println("Thread has been interrupted."); System.out.println("The interrupt flag of sleepingThread is " + Thread.currentThread().isInterrupted()); } } } } ```   該程式的輸出如下: ``` Thread has been interrupted. The interrupt flag of sleepingThread is false ```   在上面的例子中,主執行緒在SleepingThread處於TIMED_WAITING狀態時將其中斷,此時SleepingThread被喚醒並丟擲了一個InterruptedException異常。也就是說,處於WAITING或者TIMED_WAITING狀態的執行緒被中斷時往往是通過InterruptedException異常(有時也會通過其他異常,例如ClosedByInterruptException異常)來進行通知的。   不過,當SleepingThread由於中斷而被喚醒時,它的中斷標記卻是false,而我們確確實實在主執行緒中已經呼叫了它的interrupt方法,這是為什麼呢?實際上,按照慣例,丟擲InterruptedException異常的方法,通常會在丟擲該異常時將當前執行緒的中斷標記重置為false。這是因為,當捕獲到InterruptedException異常時,我們已經知道執行緒被中斷了,那麼此時的中斷標記對於我們來說已經沒用了,但是我們還需要手動將它再設定為false,方便下次使用。因此,為了使用方便,方法在丟擲InterruptedException異常之前應該將當前執行緒的終端標記重置為false。 #### (4)BLOCKED   只有在等待一個物件的監視器的執行緒才會處於BLOCKED狀態。下面我們通過一個例子來演示對處於BLOCKED狀態的執行緒呼叫interrupt方法會發生什麼。 ```java public class BlockedDemo { private static final Integer foo = 1; public static void main(String[] args) { Thread thread1 = new Thread1(); thread1.start(); Thread thread2 = new Thread2(); thread2.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("The state of thread2 is " + thread2.getState()); thread2.interrupt(); System.out.println("The interrupt flag of thread2 is " + thread2.isInterrupted()); System.out.println("The state of thread2 is " + thread2.getState()); } private static class Thread1 extends Thread { @Override public void run() { synchronized (foo) { for (int i = 0; i < 5; i++) { fibonacci(40); } } } private int fibonacci(int n) { if (n == 1 || n == 2) { return 1; } return fibonacci(n - 1) + fibonacci(n - 2); } } private static class Thread2 extends Thread { @Override public void run() { System.out.println("Thread2 tries to get the monitor of object foo."); synchronized (foo) { System.out.println("Thread2 got the monitor of object foo."); System.out.println("The interrupt flag of thread2 is " + Thread.currentThread().isInterrupted()); } } } } ```   該程式的輸出如下: ``` Thread2 tries to get the monitor of object foo (1) The state of thread2 is BLOCKED (2) The interrupt flag of thread2 is true (3) The state of thread2 is BLOCKED (4) Thread2 got the monitor of object foo (5) The interrupt flag of thread2 is true (6) ```   下面依次分析每一條輸出:   (1)Thread2啟動,嘗試獲取foo物件的監視器;   (2)由於Thread1先獲取到了foo物件的監視器且持有較長時間,Thread2需要等待Thread1釋放foo物件的監視器,因此Thread2進入BLOCKED狀態;   (3)對處於BLOCKED狀態的Thread2執行interrupt方法,該執行緒的中斷標記變成true;   (4)對Thread2執行interrupt方法後,該執行緒仍然處於BLOCKED狀態;   (5)因為Thread1釋放了foo物件的監視器,所以Thread2獲取到了該監視器,繼續執行下面的程式碼;   (6)Thread2重新進入RUNNABLE狀態後,中斷標記仍然為true,此時可以根據中斷標記來決定之後的邏輯。   綜上,對處於BLOCKED狀態的執行緒呼叫interrupt方法,僅僅只是將該執行緒的中斷標記設定為true,除此之外沒有任何