4.java併發程式設計基礎
一、執行緒簡介
1.1、什麼是執行緒
1.2、為什麼要使用多執行緒
1.3、執行緒優先順序
1.4、執行緒的狀態
Java執行緒在執行的生命週期中可能處於表4-1所示的6種不同的狀態;
1.5、Daemon執行緒
Daemon執行緒是一種支援型執行緒,因為它主要被用作程式中後臺排程以及支援性工作。這意味著,當一個Java虛擬機器中不存在非Daemon執行緒的時候,Java虛擬機器將會退出。可以通過呼叫Thread.setDaemon(true)將執行緒設定為Daemon執行緒。
Daemon執行緒被用作完成支援性工作,但是在Java虛擬機器退出時Daemon執行緒中的finally塊並不一定會執行。
main執行緒(非Daemon執行緒)在啟動了執行緒DaemonRunner之後隨著main方法執行完畢而終止,而此時Java虛擬機器中已經沒有非Daemon執行緒,虛擬機器需要退出。Java虛擬機器中的所有Daemon執行緒都需要立即終止,因此DaemonRunner立即終止,但是DaemonRunner中的finally塊並沒有執行。
**注意 **在構建Daemon執行緒時,不能依靠finally塊中的內容來確保執行關閉或清理資源的邏輯
二、啟動和終止執行緒
2.1、構造執行緒
2.2、啟動執行緒
2.3、理解中斷
阻塞庫方法::比如說,一個執行緒因為等待資源,而無限阻塞,這是時候,可以呼叫interrupt()方法將執行緒進行中斷(丟擲異常)
不呼叫阻塞庫方法::獲取中斷標誌,然後進行修改,然後進行 自己的邏輯操作;
中斷可以理解為執行緒的一個標識位屬性,它表示一個執行中的執行緒是否被其他執行緒進行了中斷操作。中斷好比其他執行緒對該執行緒打了個招呼,其他執行緒通過呼叫該執行緒的interrupt()方法對其進行中斷操作。
執行緒通過檢查自身是否被中斷來進行響應,執行緒通過方法isInterrupted()來進行判斷是否被中斷,也可以呼叫靜態方法Thread.interrupted()對當前執行緒的中斷標識位進行復位。如果該執行緒已經處於終結狀態,即使該執行緒被中斷過,在呼叫該執行緒物件的isInterrupted()時依舊會返回false。
從Java的API中可以看到,許多宣告丟擲InterruptedException的方法(例如Thread.sleep(long millis)方法)這些方法在丟擲InterruptedException之前,Java虛擬機器會先將該執行緒的中斷標識位清除,然後丟擲InterruptedException,此時呼叫isInterrupted()方法將會返回false。
在程式碼清單4-7所示的例子中,首先建立了兩個執行緒,SleepThread和BusyThread,前者不停地睡眠,後者一直執行,然後對這兩個執行緒分別進行中斷操作,觀察二者的中斷標識位
SleepThread interrupted is false
BusyThread interrupted is true
從結果可以看出,丟擲InterruptedException的執行緒SleepThread,其中斷標識位被清除了,而一直忙碌運作的執行緒BusyThread,中斷標識位沒有被清除。
2.4、過期的suspend()、resume()和stop()
大家對於CD機肯定不會陌生,如果把它播放音樂比作一個執行緒的運作,那麼對音樂播放做出的暫停、恢復和停止操作對應線上程Thread的API就是suspend()、resume()和stop()。
被 wait()和 notiafy 取代
2.5、安全地終止執行緒
在4.2.3節中提到的中斷狀態是執行緒的一個標識位,而中斷操作是一種簡便的執行緒間互動方式,而這種互動方式最適合用來取消或停止任務。除了中斷以外,還可以利用一個boolean變數來控制是否需要停止任務並終止該執行緒。
在程式碼清單4-9所示的例子中,建立了一個執行緒CountThread,它不斷地進行變數累加,而主執行緒嘗試對其進行中斷操作和停止操作。
public class Shutdown {
public static void main(String[] args) throws Exception {
Runner one = new Runner();
Thread countThread = new Thread(one, "CountThread");
countThread.start();
// 睡眠1秒,main執行緒對CountThread進行中斷,使CountThread能夠感知中斷而結束
TimeUnit.SECONDS.sleep(1);
countThread.interrupt();
Runner two = new Runner();
countThread = new Thread(two, "CountThread");
countThread.start();
// 睡眠1秒,main執行緒對Runner two進行取消,使CountThread能夠感知on為false而結束
TimeUnit.SECONDS.sleep(1);
two.cancel();
}
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()){
i++;
}
System.out.println("Count i = " + i);
}
public void cancel() {
on = false;
}
}
}
示例在執行過程中,main執行緒通過中斷操作和cancel()方法均可使CountThread得以終止。(利用自定義標誌位終止)
這種通過標識位或者中斷操作的方式能夠使執行緒在終止時有機會去清理資源,而不是武斷地將執行緒停止,因此這種終止執行緒的做法顯得更加安全和優雅。
三、執行緒間通訊
3.1、volatile和synchronized關鍵字
Java支援多個執行緒同時訪問一個物件或者物件的成員變數,由於每個執行緒可以擁有這個變數的拷貝,所以程式在執行過程中,一個執行緒看到的變數並不一定是最新的。(需要volatile保證可見性)
關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個執行緒在同一個時刻,只能有一個執行緒處於方法或者同步塊中,它保證了執行緒對變數訪問的可見性和排他性。
對於同步塊的實現使用了monitorenter和monitorexit指令,而同步方法則是依靠方法修飾符上的ACC_SYNCHRONIZED來完成的。無論採用哪種方式,其本質是對一個物件的監視器(monitor)進行獲取,而這個獲取過程是排他的,也就是同一時刻只能有一個執行緒獲取到由synchronized所保護物件的監視器。
任意一個物件都擁有自己的監視器,當這個物件由同步塊或者這個物件的同步方法呼叫時,執行方法的執行緒必須先獲取到該物件的監視器才能進入同步塊或者同步方法,而沒有獲取到監視器(執行該方法)的執行緒將會被阻塞在同步塊和同步方法的入口處,進入BLOCKED狀態
從圖4-2中可以看到,任意執行緒對Object(Object由synchronized保護)的訪問,首先要獲得Object的監視器。如果獲取失敗,執行緒進入同步佇列,執行緒狀態變為BLOCKED。當訪問Object的前驅(獲得了鎖的執行緒)釋放了鎖,則該釋放操作喚醒阻塞在同步佇列中的執行緒,使其重新嘗試對監視器的獲取
3.2、等待/通知機制
if換while
3.3、等待/通知的經典範式
從4.3.2節中的WaitNotify示例中可以提煉出等待/通知的經典範式,該正規化分為兩部分,分別針對等待方(消費者)和通知方(生產者)。
等待方遵循如下原則。
-
1)獲取物件的鎖。
-
2)如果條件不滿足,那麼呼叫物件的wait()方法,被通知後仍要檢查條件。
-
3)條件滿足則執行對應的邏輯。
對應的虛擬碼如下:一定要while,需要滿足第二、三條,所以不能用IF
synchronized(物件) {
while(條件不滿足) {
物件.wait();
}
對應的處理邏輯
}
通知方遵循如下原則。
-
1)獲得物件的鎖。
-
2)改變條件。
-
3)通知所有等待在物件上的執行緒。
對應的虛擬碼如下:
synchronized(物件) {
改變條件
物件.notifyAll();
}
3.4、管道輸入/輸出流
管道輸入/輸出流和普通的檔案輸入/輸出流或者網路輸入/輸出流不同之處在於,它主要用於執行緒之間的資料傳輸,而傳輸的媒介為記憶體。
管道輸入/輸出流主要包括瞭如下4種具體實現:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前兩種面向位元組,而後兩種面向字元。
在程式碼清單4-12所示的例子中,建立了printThread,它用來接受main執行緒的輸入,任何main執行緒的輸入均通過PipedWriter寫入,而printThread在另一端通過PipedReader將內容讀出並列印。
public class Piped {
public static void main(String[] args) throws Exception {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
// 將輸出流和輸入流進行連線,否則在使用時會丟擲IOException
out.connect(in);
Thread printThread = new Thread(new Print(in), "PrintThread");
printThread.start();
int receive = 0;
try {
while ((receive = System.in.read()) != -1) {
out.write(receive);
}
} finally {
out.close();
}
}
static class Print implements Runnable {
private PipedReader in;
public Print(PipedReader in) {
this.in = in;
}
public void run() {
int receive = 0;
try {
while ((receive = in.read()) != -1) {
System.out.print((char) receive);
}
} catch (IOException ex) {}
}
}
}
執行該示例,輸入一組字串,可以看到被printThread進行了原樣輸出。
Repeat my words.
Repeat my words.
對於Piped型別的流,必須先要進行繫結,也就是呼叫connect()方法,如果沒有將輸入/輸出流繫結起來,對於該流的訪問將會丟擲異常。
3.5、Thread.join()的使用
每個執行緒終止的前提是前驅執行緒的終止,每個執行緒等待前驅執行緒終止後,才從join()方法返回,這裡涉及了等待/通知機制。(上面執行緒執行完才能執行下面的)
3.6、ThreadLocal的使用
ThreadLocal,即執行緒變數,是一個以ThreadLocal物件為鍵、任意物件為值的儲存結構。這個結構被附帶線上程上,也就是說一個執行緒可以根據一個ThreadLocal物件查詢到繫結在這個執行緒上的一個值。
四、執行緒應用例項
4.1、等待超時模式
等待超時模式就是在等待/通知正規化基礎上增加了超時控制,這使得該模式相比原有正規化更具有靈活性,因為即使方法執行時間過長,也不會“永久”阻塞呼叫者,而是會按照呼叫者的要求“按時”返回
4.2、一個簡單的資料庫連線池示例
4.3、執行緒池技術及其示例
4.4、一個基於執行緒池技術的簡單Web伺服器
五、本章小結
本章從介紹多執行緒技術帶來的好處開始,講述瞭如何啟動和終止執行緒以及執行緒的狀態,詳細闡述了多執行緒之間進行通訊的基本方式和等待/通知經典範式。線上程應用示例中,使用了等待超時、資料庫連線池以及簡單執行緒池3個不同的示例鞏固本章前面章節所介紹的Java多執行緒基礎知識。最後通過一個簡單的Web伺服器將上述知識點串聯起來,加深我們對這些知識點的理解