Java多線程編程(6)--線程間通信(下)

  因爲本文的內容大部分是以生產者/消費者模式來進行講解和舉例的,所以在開始學習本文介紹的幾種線程間的通信方式之前,我們先來熟悉一下生產者/消費者模式。
  在實際的軟件開發過程中,經常會碰到如下場景:某個模塊負責產生數據(可能是消息、文件、任務等),這些數據由另一個模塊來負責處理。產生數據的模塊,就形象地被稱爲生產者;而處理數據的模塊,就被稱爲消費者。
  單單抽象出生產者和消費者,還稱不上是生產者/消費者模式。該模式還需要有一個緩衝區處於生產者和消費者之間來作爲一箇中介。生產者把數據放入緩衝區,而消費者從緩衝區取出數據。因此,生產者/消費者模式大概的結構如下圖:

  我們可以使用不同的線程來模擬生產者和消費者。對應的,生產數據的線程就被稱爲是生產者線程,而消費數據的線程就被稱爲是消費者。而緩衝區則可以選用那些線程安全的數據結構來模擬,因此,緩衝區在這裏就起到了線程間通信工具的作用。本文將會通過生產者/消費者模式作爲例子來介紹幾種線程間的通信方式。

一.阻塞隊列

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<? super E> c)
    一次性從隊列中取出所有可用的數據對象放在指定的集合中。
  • int drainTo​(Collection<? super E> 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接口定義的方法:

  除了這幾個方法外,該隊列其他方法的行爲和LinkedBlockingQueue類似,這裏不再過多贅述。

(7)LinkedBlockingDeque

  顧名思義,這個類是用鏈表實現的阻塞雙端隊列,和LinkedBlockingQueue類似,它既可以是有界的,也可以是無界的。實際上,除了具有雙端隊列的特性外,該類與LinkedBlockingQueue十分相似,可以參照上面對LinkedBlockingQueue的介紹來理解它,這裏不再詳細介紹。

二.信號量Semaphore

  Semaphore類是一個計數信號量。爲了便於討論,我們把代碼所訪問的特定資源或者執行特定操作的機會同意看作是一種資源,可以將其稱之爲虛擬資源。Semaphore相當於虛擬資源訪問許可管理器,它可以用來控制同一時間內對虛擬資源的訪問次數。爲了對虛擬資源的訪問進行流量控制,我們必須使相應代碼只有在獲得許可的情況下才能夠訪問這些資源。基於這種思想,在訪問虛擬資源前應該先申請許可,在訪問後應該釋放許可。
  Semaphore的acquire和release方法分別用於申請和釋放許可。如果當前可用的許可數等於0或小於0(在構造Semaphore實例的時候可以指定許可數爲0或負數),那麼acquire方法會使執行線程暫停。Semaphore內部維護了一個隊列來存儲這些被暫停的線程,默認情況下,Semaphore使用非公平策略,當然也可以在構造方法中顯式指定Semaphore實例使用公平策略還是非公平策略。
  下面是Semaphore類提供的所有方法:

三.管道流

  Java語言提供了各種各樣的輸入/輸出流,使我們能夠很方便地對數據進行操作,其中管道流是一種特殊的流,用於在不同的線程間直接傳送數據。一個線程發送數據到管道,另一個線程從管道中讀取數據。通過使用管道,可以實現不同線程間的通信,而無需藉助臨時文件等數據中介。
  和其他流類似,管道流也分爲字節流和字符流,其中字節流對應的輸入流和輸出流分別是PipedInputStream和PipedOutputStream,字符流對應的輸入流和輸出流分別是PipedReader和PipedWriter。
  管道流實際上是使用一個循環緩衝數組來實現的,輸入流從這個數組中讀數據,輸出流向這個數組中寫數據。當緩衝區滿時,輸出流所在的線程將會阻塞,當緩衝區空時,輸入流所在的線程將會阻塞。這個數組位於輸入流內部,默認大小爲1024,也可以通過管道輸入流的構造方法來指定緩衝數組大小。
  管道輸入流和輸出流在使用之前必須先建立連接,可以通過輸入流或輸出流的構造方法或connect方法來使兩個流建立連接。假設in是線程A的輸入流,out是線程B的輸出流,那麼可以通過以下幾種方法來建立連接:

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:

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方法:

  下面是一個使用Exchanger的例子:

import java.util.concurrent.Exchanger;

public class ExchangerDemo {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();
        new Buyer(exchanger).start();
        new Seller(exchanger).start();
    }

    private static class Buyer extends Thread {
        private Exchanger<String> exchanger;

        Buyer(Exchanger<String> 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<String> exchanger;

        Seller(Exchanger<String> 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類中與中斷有關的三個方法:

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,也不會產生其他的行爲。

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外不會產生其他行爲,下面我們來做個實驗:

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方法修改如下:

@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狀態後將其中斷:

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方法會發生什麼。

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,除此之外沒有任何變化。

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