第 14 章 多線程
本章內容:
* 什麼是線程
* 中斷線程
* 線程狀態
* 線程屬性
* 同步
* 阻塞隊列
* 線程安全的集合
* Collable與Future
* 執行器
* 同步器
* 線程與Swing
- 通常,每一個任務稱爲一個線程(thread),它是線程控制的簡稱。可以同時運行一個以上線程的程序稱爲多線程程序(multithreaded)。
- 多進程與多線程有哪些區別呢?本質的區別在於每個進程擁有自己的一整套變量,而線程則共享數據。共享變量使線程之間的通信比進程之間的通信更有效、更容易。此外,在有些操作系統中,與進程相比較,線程更“輕量級”,創建、撤銷一個線程比啓動新進程的開銷要小得多。
14.1 什麼是線程
- Thread 類的靜態 sleep 方法將暫停給定的毫秒數。調用 Thread.sleep 不會創建一個新線程, sleep 是 Thread 類的靜態方法,用於暫停當前線程的活動。 sleep 方法可以拋出一個 InterruptedException 異常。
- java.lang.Thread 1.0
- static void sleep(long millis)
休眠給定的毫秒數。
參數:millis 休眠的毫秒數
- static void sleep(long millis)
- 不要調用 Thread 類或 Runnable 對象的 run 方法。直接調用 run 方法,只會執行同一個線程中的任務,並不會啓動新線程。應該調用 Thread.start 方法。這個方法將創建一個執行 run 方法的新線程。
- java.lang.Thread 1.0
- Thread(Runnable target)
構造一個新線程,用於調用給定target的run()方法。 - void start()
啓動這個線程,將引發調用run()方法。這個方法將立即返回,並且新線程將並行運行。 - void run()
調用關聯Runnable的run方法。
- Thread(Runnable target)
- java.lang.Runnable 1.0
- void run()
必須覆蓋這個方法,並在這個方法中提供所要執行的任務指令。
- void run()
14.2 中斷線程
- 當線程的 run 方法執行方法體重最後一條語句後,並經由執行 return 語句返回時,或者出現了在方法中沒有捕獲的異常時,線程將終止。在Java的早期版本中,還有一個 stop 方法,其他線程可以調用它終止線程。但是,這個方法現在已經被棄用了。
- 有一種可以強制線程終止的方法。然而, interrupt 方法可以用來請求終止線程。
- 當對一個線程調用 interrupt 方法時,線程的中斷狀態將被置位。這是每一個線程都具有的 boolean 標誌。每個線程都應該不時地檢查這個標誌,以判斷線程是否被中斷。
- 調用 Thread.currentThread().isInterrputed() 方法獲得當前線程的中斷狀態是否被置位。但是,如果線程被阻塞,就無法檢測中斷狀態。這是產生 InterruptedException 異常的地方。當在一個被阻塞的線程(調用 sleep 或 wait )上調用 interrupt 方法時,阻塞調用將會被 Interrupt Exception 異常中斷(存在不能被中斷的阻塞 I/O 調用,應該考慮選擇可中斷的調用)。
- 沒有任何語言方面的需求要求一個被中斷的線程應該終止。中斷一個線程不過是引起它的注意。被中斷的線程可以決定如何響應中斷。某些線程是如此重要以至於應該處理完異常後,繼續執行,而不理會中斷。但是,更普通的情況是,線程將簡單地將中斷作爲一個終止的請求。
- 如果在每次工作迭代之後都調用 sleep 方法(或者其他的可中斷方法), isInterrpted 檢測既沒有必要也沒有用處。如果在種蒜狀態被置位時調用 sleep 方法,它不會休眠。相反,它將清除這一狀態(!)並拋出 InterrputedException 。因此,如果你的循環調用 sleep ,不會檢測中斷狀態,相反,需要捕獲 InterrputedException 異常。
- 有兩個非常類似的方法, interrupted 和 isInterrupted 。 Interrupted 方法是一個靜態方法,它檢測當前的線程是否被中斷。而且,調用 interrupted 方法會清除該線程的中斷狀態。另一方面, isInterrupted 方法是一個實例方法,可用來檢驗是否有線程被中斷。調用這個方法不會改變中斷狀態。
- java.lang.Thread 1.0
- void interrupt()
向線程發送中斷請求。線程的中斷狀態將設置爲 true 。如果目前該線程被一個 sleep 調用阻塞,那麼, InterruptedException 異常被拋出。 - static boolean interrupted()
測試當前線程(即正在執行這一命令的線程)是否被中斷。注意,這是一個靜態方法。這一調用會產生副作用-它將當前線程的中斷狀態重置爲false。 - boolean isInterrupted()
測試線程是否被終止。不像靜態的中斷方法,這一調用不改變線程的中斷狀態。 - static Thread currentThread()
返回代表當前執行線程的 Thread 對象。
- void interrupt()
14.3 線程狀態
- 線程可以有如下6種狀態:
- New (新創建)
- Runnable (可運行)
- Blocked (被阻塞)
- Waiting (等待)
- Timed waiting (計時等待)
- Terminated (被終止)
14.3.1 新創建線程
- 當用new操作符創建一個新線程,如 new Thread(r) ,該線程還沒有開始運行。這意味着它的狀態是 new 。當一個線程處於新創建狀態時,程序還沒有開始運行線程中的代碼。在線程運行之前還有一些基本工作要做。
14.3.2 可運行線程
- 一旦調用 start 方法,線程處於 runnable 狀態。一個可運行的線程可能正在運行也可能沒有運行,這取決於操作系統給線程提供運行的時間。( Java 的規範說明沒有將它作爲一個單獨狀態。一個正在運行中的線程仍然處於可運行狀態。)
- 一旦一個線程開始運行,它不必始終保持運行。事實上,運行中的線程被中斷,目的是爲了讓其他線程獲得運行機會。線程調度的細節依賴於操作系統提供的服務。搶佔式調度系統給每一個可運行線程一個時間片來執行任務。當時間片用完,操作系統剝奪該線程的運行權,並給另一個線程運行機會。當選擇下一個線程時,操作系統考慮線程的優先級。
- 在任何給定時刻,一個可運行的線程可能正在運行也可能沒有運行(這就是爲什麼將這個狀態稱爲可運行而不是運行)。
14.3.3 被阻塞線程和等待線程
- 當線程處於被阻塞或等待狀態時,它暫時不活動。它不運行任何代碼且消耗最少資源。直到線程調度器重新激活它。細節取決於它是怎樣達到非活動狀態的。
- 當一個線程試圖獲取一個內部的對象鎖(而不是 java.util.concurrect 庫中的鎖),而該鎖被其他線程持有,則該線程進入阻塞狀態。當所有其他線程釋放該鎖,並且線程調度器允許本線程持有它的時候,該線程將變成非阻塞狀態。
- 當線程等待另一個線程通知調度器一個條件時,它自己進入等待狀態。在調用 Object.wait 方法或 Thread.join 方法,或者是等待 java.util.concurrent 庫中的 Lock 或 Condition 時,就會出現這種情況。實際上,被阻塞狀態與等待狀態是由很大不同的。
- 有幾個方法有一個超時參數。調用它們導致線程進入計時等待( timed waiting )狀態。這一狀態將一直保持到超時期滿或者接收到適當的通知。帶有超時參數的方法有 Thread.sleep 和 Object.wait 、 Thread.join 、 Lock.tryLock 以及 Condition.awit 的計時版。
- 當一個線程被阻塞或等待時(或終止時),另一個線程被調度爲運行狀態。當一個線程被重新激活(例如,因爲超時期滿或成功地獲得一個鎖),調度器檢查它是否具有比當前運行線程更高的優先級。如果是這樣,調度器從當前運行線程中挑選一個,剝奪其運行權,選擇一個新的線程運行。
- 線程狀態圖
14.3.4 被終止的線程
- 線程因如下兩個原因之一而被終止:
- 因爲 run 方法正常退出而自然死亡。
- 因爲一個沒有捕獲的異常終止了 run 方法而意外死亡。
特別是,可以調用線程的 stop 方法殺死一個線程。該方法拋出 ThreadDeath 錯誤對象,由此殺死線程。但是, stop 方法已過時,不要在自己的代碼中調用這個方法。
- java.lang.Thread 1.0
- void join()
等待終止指定的線程。 - void join(long millis)
等待指定的線程死亡或者經過指定的毫秒數。 - Thread.State getState() 5.0
得到這一線程的狀態:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING或TERMINATED之一。 - void stop()
停止該線程。這一方法已過時。 - void suspend()
暫停這一線程的執行。這一方法已過時。 - void resume()
恢復線程。這一方法僅僅在調用suspend()之後調用。這一方法已過時。
- void join()
14.4 線程屬性
- 線程的各種屬性,其中包括:線程優先級、守護線程、線程組以及處理未捕獲異常的處理器。
14.4.1 線程優先級
- 在 Java 程序設計語言中,每一個線程有一個優先級。默認情況下,一個線程繼承它的父線程的優先級。可以用 setPriority 方法提高或降低任何一個線程的優先級。可以將優先級設置爲在 MIN_PRIORITY (在 Thread 類中定義爲 1 )與 MAX_PRIORITY (定義爲 10 )之間的任何值。 NORM_PRIORITY 被定義爲 5 。
- 每當線程調度器有機會選擇新線程時,它首先選擇具有較高優先級的線程。但是,線程優先級是高度依賴於系統的。當虛擬機依賴於宿主機平臺的線程實現機制時, Java 線程的優先級被映射到宿主主機平臺的優先級上,優先級個數也許更多,也許更少。
Windows 有 7 個優先級別。一些 Java 優先級將映射到相同的操作系統優先級。在 Sun 爲 Linux 提供的 Java 虛擬機,線程的優先級被忽略-所有線程具有相同的優先級。 - java.lang.Thread 1.0
- void setPriority(int newPriority)
設置線程的優先級。優先級必須在Thread.MIN_PRIORITY與Thread.MAX_PRIORITY之間。一般使用Thread.NORM_PRIORITY優先級。 - static int MIN_PRIORITY
線程的最小優先級。最小優先級的值爲1。 - static int NORM_PRIORITY
線程的默認優先級。默認優先級爲5。 - static int MAX_PRIORITY
線程的最高優先級。最高優先級的值爲10。 - static void yield()
導致當前執行線程處於讓步狀態。如果有其他的可運行線程具有至少與此線程同樣高的優先級,那麼這些線程接下來會被調度。注意,這是一個靜態方法。
- void setPriority(int newPriority)
14.4.2 守護線程
- 可以通過
t.setDaemon(true)
將線程轉換爲守護線程( daemon thread )。守護線程的唯一用途是爲其他線程提供服務。計時線程就是一個例子,它定時地發送“計時器嘀嗒”信號給其他線程或清空過時的高速緩存項的線程。當只剩下守護線程時,虛擬機就退出了,由於如果只剩下守護線程,就沒必要繼續運行程序了。 - 守護線程應該永遠不去訪問固有資源,如文件、數據庫,因爲它會在任何時候甚至在一個操作的中間發生中斷。
- java.lang.Threed 1.0
- void setDaemon(boolean isDaemon)
標識該線程爲守護線程或用戶線程。這一方法必須在線程啓動之前調用。
- void setDaemon(boolean isDaemon)
14.4.3 未捕獲異常處理器
- 線程的 run 方法不能拋出任何被檢測的異常,但是,不被檢測的異常會導致線程終止。在這種情況下,線程就死亡了。
但是,不需要任何 catch 子句來處理可以被傳播的異常。相反,就在線程死亡之前,異常被傳遞到一個被用於未捕獲異常的處理器。
該處理器必須屬於一個實現 Thread.UncaughtExceptionHandler 接口的類。這個接口只有一個方法。
可以用 setUncaughtExceptionHandler 方法爲任何線程安裝一個處理器。也可以用 Thread 類的靜態方法 setDefaultUncaughtExceptionHandler 爲所有線程安裝一個默認的處理器。替換處理器可以使用日誌API發送未捕獲異常的報告到日誌文件。void uncaughtException(Thread t,Throwable e)
如果不安裝默認的處理器,默認的處理器爲空。但是,如果不爲獨立的線程安裝處理器,此時的處理器就是該線程的 ThreadGroup 對象。 - 線程組是一個可以統一管理的線程集合。默認情況下,創建的所有線程屬於相同的線程組,但是,也可能會建立其他的組。現在引入了更好的特性用於線程集合的操作,所以建議不要在自己的程序中使用線程組。
- ThreadGroup 類實現 Thread.UncaughtExceptionHandler 接口。它的 uncaughtException 方法做如下操作:
1)如果該線程組有父線程組,那麼父線程組的 uncaughtException 方法被調用。
2)否則,如果 Thread.getDefaultExceptionHandler 方法返回一個非空的處理器,則調用該處理器。
3)否則,如果 Throwable 是 ThreadDeath 的一個實例,什麼都不做。
4)否則,線程的名字以及 Throwable 的棧跟蹤被輸出到 System.err 上。 - java.lang.Thread 1.0
- static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler) 5.0
- static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() 5.0
設置或獲取未捕獲異常的默認處理器。 - void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler) 5.0
- Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() 5.0
設置或獲取未捕獲異常的處理器。如果沒有安裝處理器,則將線程組對象作爲處理器。
- java.lang.Thread.UncaughtExceptionHandler 5.0
- void uncaughtException(Thread t,Throwable e)
當一個線程因未捕獲異常而終止,按規定要將客戶報告記錄到日誌中。
參數:t 由於未捕獲異常而終止的線程
e 未捕獲的異常對象
- void uncaughtException(Thread t,Throwable e)
- java.lang.ThreadGroup 1.0
- void uncaughtException(Thread t,Throwable e)
如果有父線程組,調用父線程組的這一方法;或者,如果 Thread 類有默認處理器,調用該處理器,否則,輸出棧蹤跡到標準錯誤流上(但是,如果 e 是一個 ThreadDeath 對象,棧蹤跡是被禁用的。 ThreadDeath 對象 stop 方法產生,而該方法已經過時)。
- void uncaughtException(Thread t,Throwable e)
14.5 同步
- 如果兩個線程存取相同的對象,並且每一個線程都調用了一個修改該對象狀態的方法,這樣一個情況通常稱爲競爭條件( race condition )。
14.5.1 競爭條件的一個例子
- 銀行轉賬例子
14.5.2 競爭條件詳解
- 一條名利是由幾條指令組成的,執行它們的線程可以在任何一條指令點上被中斷。
14.5.3 鎖對象
- 有兩種機制防止代碼塊受併發訪問的干擾。Java語言提供一個 synchronized 關鍵字達到這一目的,並且 Java SE 5.0 引入了 ReentrantLock 類。 synchronized 關鍵字自動提供了一個鎖以及相關的“條件”,對於大多數需要顯示鎖的情況,這是很遍歷的。 java.util.concurrent 框架爲這些基礎機制提供獨立的類。
- 用 ReentrantLock 保護代碼塊的基本結構如下:
這一結構確保任何時刻只有一個線程進入臨界區。一旦一個線程封鎖了鎖對象,其他任何線程都無法通過 lock 語句。當其他線程調用 lock 時,它們被阻塞,直到第一個線程釋放鎖對象。myLock.lock(); //a ReentrantLock object try { critical section } finally { myLock.unlock();//make sure the lock is unlocked even if an exception is three }
- 把解鎖操作放在 finally 子句之內是至關重要的。如果在臨界區的代碼拋出異常,鎖必須被釋放。否則,其他線程將永遠阻塞。
- 如果使用鎖,就不能使用帶資源的 try 語句。首先,解鎖方法名不是 close 。不過,即使將它重命名,帶資源的 try 語句也無法正常工作。它的首部希望聲明一個新變量。但是如果使用一個鎖,可能想使用多個線程共享的那個變量(而不是新變量)。
- 鎖是可重入的,因爲線程可以重複地獲得已經持有的鎖。鎖保持一個持有計數( hold count )來跟蹤對 lock 方法的嵌套調用。線程在每一次調用lock都要調用 unlock 來釋放鎖。由於這一特性,被一個鎖保護的代碼可以調用另一個使用相同的鎖的方法。
- 通常,可能想要保護需若干個操作來更新或檢查共享對象的代碼塊。要確保這些操作完成後,另一個線程才能使用相同對象。
- 要留心臨界區中的代碼,不要因爲異常的拋出而跳出了臨界區。如果在臨界區代碼結束之前拋出了異常, finally 子句將釋放鎖,但會使對象可能處於一種受損狀態。
- java.util.concurrent.locks.Lock 5.0
- void lock()
獲取這個鎖;如果鎖同時被另一個線程擁有則發生阻塞。 - void unlock()
釋放這個鎖。
- void lock()
- java.util.concurrent.locks.ReentrantLock 5.0
- ReentrantLock()
構建一個可以被用來保護臨界區的可重入鎖。 - ReentrantLock(boolean fair)
構建一個帶有公平策略的鎖。一個公平鎖偏愛等待時間最長的線程。但是麼這一公平的保證將大大降低性能。所以,默認情況下,鎖沒有被強制爲公平的。
- ReentrantLock()
- 聽起來公平鎖更合理一些,但是使用公平鎖比使用常規鎖要慢很多。只有當你確實瞭解自己要做什麼並且對於你要解決的問題有一個特定的理由必須使用公平鎖的時候,纔可以使用公平鎖。即使使用公平鎖,也無法確保線程調度器是公平的。如果線程調度器選擇忽略一個線程,而該線程爲了這個鎖已經等待了很長時間,那麼就沒有機會公平地處理這個鎖了。
14.5.4 條件對象
- 通常,線程進入臨界區,卻發現在某一條件滿足之後它才能執行。要使用一個條件對象來管理那些已經獲得了一個鎖但是卻不能做有用工作的線程。由於歷史的原因,條件對象經常被稱爲條件變量( conditional variable )。
- 一個鎖對象可以有一個或多個相關的條件對象。可以用 newCondition 方法獲得一個條件對象。習慣上給每一個條件對象命名爲可以反映它鎖表達的條件的名字。如果條件不滿足,調用 Condition.await() 。當前線程現在被阻塞了,並放棄了鎖。
- 等待獲得鎖的線程和調用 await 方法的線程存在本質上的不同。一旦一個線程調用 await 方法,它進入該條件的等待集。當鎖可用時,該線程不能馬上解除阻塞。相反,它處於阻塞狀態,直到另一個線程調用同一條件上的 singalAll 方法時爲止。 singalAll() 調用重新激活因爲這一條件而等待的所有線程。當這些線程從等待集中移出時,它們再次成爲可運行的,調度器將再次激活它們。同時,它們將試圖重新進入該對象。一旦鎖成爲可用的,它們中的某個將從 await 調用返回,獲得該鎖並從被阻塞的地方繼承執行。
此時,線程應該再次測試該條件。由於無法確保該條件被滿足 -signalAll 方法僅僅是通知正在等待的線程:此時有可能已經滿足條件,值得再次去檢測該條件。 - 通常,對 await 的調用應該在如下形式的循環體中:
while(!(ok to proceed)) condition.await();
- 至關重要的是最終需要某個其他線程調用 signalAll 方法。當一個線程調用 await 時,它沒有辦法重新激活自身。它寄希望於其他線程。如果沒有其他線程來重新激活等待的線程,它就永遠不再運行了。這將導致令人不快的死鎖( deadlock )現象。如果所有其他線程被阻塞,最後一個活動線程在解除其他線程的阻塞狀態之前就調用 await 方法,那麼它也被阻塞。沒有任何線程可以解除其他線程的阻塞,那麼該程序就掛起了。
- 應該何時調用 signalAll 呢?經驗上講,在對象的狀態有利於等待線程的方向改變時調用 signalAll 。
- 注意調用 signalAll 不會立即激活一個等待線程。它僅僅解除等待線程的阻塞,以便這些線程可以在當前線程退出同步方法之後,通過競爭實現對對象的訪問。
- 另一個方法 signal ,則是隨機解除等待集中某個線程的阻塞狀態。這比解除所有線程的阻塞更加有效,但也存在危險。如果隨機選擇的線程發現自己仍然不能運行,那麼它再次被阻塞。如果沒有其他線程再次調用 signal ,那麼系統就死鎖了。
- 當一個線程擁有某個條件的鎖時,它僅僅可以在該條件上調用 await 、 signalAll 或 signal 方法。
- java.util.concurrent.locks.Lock 5.0
- Condition newCondition()
返回一個與該鎖相關的條件對象。
- Condition newCondition()
- java.util.concurrent.locks.Condition 5.0
- void await()
將該線程放到條件的等待集中。 - void signalAll()
解除該條件的等待集中的所有線程的阻塞狀態。 - void signal()
從該條件的等待集中隨機地選擇一個線程,解除其阻塞狀態。
- void await()
14.5.5 synchronized關鍵字
- 鎖和條件的關鍵之處:
- 鎖用來保護代碼片段,任何時刻只能有一個線程執行被保護的代碼。
- 鎖可以管理試圖進入被保護代碼段的線程。
- 鎖可以擁有一個或多個相關的條件對象。
- 每個條件對象管理那些已經進入被保護的代碼段但還不能運行的線程。
- 從 1.0 版開始, Java 中的每一個對象都有一個內部鎖。如果一個方法用 synchronized 關鍵字聲明,那麼對象的鎖將保護整個方法。也就是說,要調用該方法,線程必須獲得內部的對象鎖。
- 內部對象鎖只有一個相關條件。 wait 方法添加一個線程到等待集中, notifyAll/notify 方法解除等待線程的阻塞狀態。換句話說,調用 wait 或 notifyAll 等價於
intrinsicCondition.await(); intrinsicCondition.signalAll();
- wait 、 notifyAll 以及 notify 方法是 Object 類的 final 方法。 Condition 方法必須被命名爲 await 、 signalAll 和 signal 以便它們不會與那些方法發生衝突。
- 將靜態方法聲明爲 synchronized 也是合法的。如果調用這個方法,該方法獲得相關的類對象的內部類。因此,沒有其他線程可以調用同一個類的這個或任何其他的同步靜態方法。
- 內部鎖和條件存在一些侷限。包括:
- 不能中斷一個正在試圖獲得鎖的線程。
- 試圖獲得鎖時不能設定超時。
- 每個鎖僅有單一的條件,可能是不夠的。
- 在代碼中應該使用哪一種? Lock 和 Conditon 對象還是同步方法?下面是一些建議:
- 最好既不使用 Lock/Condition 也不實用 synchronized 關鍵字。在許多情況下你可以使用 java.util.concurrent 包中的一種機制,它會爲你處理所有的加鎖。
- 如果 synchronized 關鍵字適合你的程序,那麼請儘量使用它。這樣可以減少編寫的代碼數量,減少出錯的機率。
- 如果特別需要 Lock/Condition 結構提供的特有特性時,才使用 Lock/Condition 。
- java.lang.Object 1.0
- void notifyAll()
解除那些在該對象上調用 wait 方法的線程的阻塞狀態。該方法只能在同步方法或同步塊內部調用。如果當前線程不是對象鎖的持有者,該方法拋出一個 IllegalMonitorStateException 異常。 - void notify()
隨機選擇一個在該對象上調用 wait 方法的線程,解除其阻塞狀態。改方法只能在一個同步方法或同步塊中調用。如果當前線程不是對象鎖的持有者,該方法拋出一個 IllegalMonitorStateException 異常。 - void wait()
導致線程進入等待狀態直到它被通知。該方法只能在一個同步方法中調用。如果當前線程不是對象鎖的持有者,該方法拋出一個 IllegalMonitorStateException 異常。 - void wait(long millis)
- void wait(long millis,int nanos)
導致線程進入等待狀態直到它被通知或者經過指定的時間。這些方法只能在一個同步方法中調用。如果當前線程不是對象鎖的持有者該方法拋出一個 IllegalMonitorStateException 異常。
參數:millis 毫秒數
nanos 納秒數,<1 000 000
- void notifyAll()
14.5.6 同步阻塞
- 每一個 Java 對象有一個鎖。線程可以通過調用同步方法獲得鎖。還有另一種機制可以獲得鎖,通過進入一個同步阻塞。當線程進入如下形式的阻塞:
於是它獲得 obj 的鎖。synchronized(obj) //this is the synchronized block { critical section }
- 使用一個對象的鎖來實現額外的原子操作,實際上稱爲客戶端鎖定( client-slide locking )。客戶端鎖定是非常脆弱的,通常不推薦使用。
14.5.7 監視器概念
- 鎖和條件是線程同步的強大工具,但是,嚴格來講,它們不是面向對象的。多年來,研究人員努力尋找一種方法,可以在不需要程序員考慮如何加鎖的情況下,就可以保證多線程的安全性。最成功的解決方案之一是監視器( monitor ),這一概念最早是由 Per Brinch Hansen 和 Tony Hoare 在 20 世紀 70 年代提出的。用 Java 的術語來講,監視器具有如下特性:
- 監視器是隻包含私有域的類。
- 每個監視器的對象有一個相關的鎖。
- 使用該鎖對所有的方法進行加鎖。
- 該鎖可以有任意多個相關條件。
- Java 設計者以不是很精確的方式採用了監視器概念, Java 中的每一個對象有一個內部的鎖和內部的條件。如果一個方法用 synchronized 關鍵字聲明,那麼,它表現的就像是一個監視器方法。通過調用 wait/notifyAll/notify 訪問條件變量。
- 在下述的 3 個方面 Java 對象不同於監視器,從而使得線程的安全性下降。
- 域不要求必須是 private 。
- 方法不要求必須是 synchronized 。
- 內部鎖對客戶是可用的。
14.5.8 Volatile 域
- 有時,僅僅爲了讀寫一個或兩個實例域就使用同步,顯得開銷過大了。畢竟,什麼地方能出錯呢?遺憾的是,使用現代的處理器與編譯器,出錯的可能性很大。
- 多處理器的計算機能夠暫時在寄存器或本地內存緩衝區中保存內存中的值。結果是,運行在不同處理器上的線程可能在同一個內存位置取到不同的值。
- 編譯器可以改變指令執行的順序以使吞吐量最大化。這種順序上的變化不會改變代碼語義,但是編譯器假定內存的值僅僅在代碼中有顯式的修改指令時纔會改變。然而,內存的值可以被另一個線程改變。
如果你使用鎖來保護可以被多個線程訪問的代碼,那麼可以不考慮這種問題。編譯器被要求通過在必要的時候刷新本地緩存來保持鎖的效應,並且不能不正當地重新排序指令。
- volatile 關鍵字爲實例域的同步訪問提供了一種免鎖機制。如果聲明一個域爲 volatile ,那麼編譯器和虛擬機就知道該域是可能被另一個線程併發更新的。
- Volatile 變量不能提供原子性。例如,方法
不能確保翻轉域中的值。private volatile boolean done; public void flipDone(){done = !done;} //not atomic
14.5.9 final 變量
- 除非使用域或 volatile 修飾符,否則無法從多個線程安全地讀取一個域。還有一種情況可以安全地訪問一個共享域,即這個域聲明爲 final 時。考慮以下聲明:
其他線程會在構造函數完成構造之後纔看到這個 accounts 變量。final Map<String,Double> accounts = new HashMap();
如果不使用 final ,就不能保證其他線程看到的是 accounts 更新後的值,它們可能都只是看到 null ,而不是新構造的 HashMap 。
對這個映射表的操作並不是線程安全的。如果多個線程在讀寫這個映射表,仍然需要進行同步。
14.5.10 原子性
- 假設對共享變量除了賦值之外並不完成其他操作,那麼可以將這些共享變量聲明爲 volatic 。
- java.util.concurrent.atomic 包中有很多類使用了很高效的機器級指令(而不是使用鎖)來保證其他操作的原子性。例如, AtomicInteger 類提供了方法 incrementAndGet 和 decrementAndGet ,它們分別以原子方式將一個整數自增或自減。可以安全地使用 AtomicInteger 作爲共享計數器而無須同步。
另外這個包中還包含 AtomicBoolean 、 AtomicLong 和 AtomicReference 以及 Boolean 值、整數、 long 值和引用的原子數組。應用程序員不應該使用這些類,它們僅供那些開發併發工具的系統程序員使用。
14.5.11 死鎖
- 有可能會因爲每一個線程要等待條件而導致所有線程都被阻塞。這樣的狀態稱爲死鎖( deadlock )。
- Java 編程語言中沒有任何東西可以避免或打破這種死鎖現象,必須仔細設計程序,以確保不會出現死鎖。
14.5.12 線程局部變量
- 有時可能要避免共享變量,使用 ThreadLocal 輔助類爲各個線程提供各自的實例。
- 要爲每個線程構造一個實例,可以使用以下代碼:
要訪問具體的格式化方法,可以調用:public static final ThreadLocal< SimpleDateFormat > dateFormat = new ThreadLocal< SimpleDateFomrat >() { protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }
在一個給定線程中首次調用 get 時,會調用 initilaValue 方法。在此之後, get 方法會返回屬於當前線程的那個實例。String dateStamp = dateFormat.get().format(new Date());
在多個線程中生成隨機數也存在類似的問題。 java.util.Random 類是線程安全的。但是如果多個線程需要等待一個共享的隨機數生成器,這會很低效。
可以使用 ThreadLocal 輔助類爲各個線程提供一個單獨的生成器,不過 Java SE 7 還另外提供一個便利類。只需要做一下調用:
ThreadLocalRandom.current() 調用會返回特定於當前線程的 Random 類實例。int random = ThreadLocalRandom.current().nextInt(upperBound);
- java.lang.ThreadLocal< T > 1.2
- T get()
得到這個線程的當前值。如果是首次調用get,會調用 initialize 來得到這個值。 - protected initialize()
應覆蓋這個方法來提供一個初始值。默認情況下,這個方法返回 null 。 - void set(T t)
爲這個線程設置一個新值。 - void remove()
刪除對應這個線程的值。
- T get()
- java.util.concurrent.ThreadLocalRandom 7
- static ThreadLocalRandom current()
返回特定於當前線程的 Random 類實例。
- static ThreadLocalRandom current()
14.5.13 鎖測試與超時
- 線程在調用 lock 方法來獲得另一個線程所持有的鎖的時候,很可能發生阻塞。應該更加謹慎地申請鎖。
- TimeUnit 是一個枚舉類型,可以取的值包括 SECONDS 、 MILLISECONDS 、 MICROSECONDS 和 NANOSECONDS 。
- lock 方法不能被中斷。如果一個線程在等待獲得一個鎖時被中斷,中斷線程在獲得鎖之前一直處於阻塞狀態。如果出現死鎖,那麼, lock 方法就無法終止。
- 然而,如果調用帶有用超時參數的 tryLock ,那麼如果線程在等待期間被中斷,將拋出 InterruptedException 異常。這是一個非常有用的特性,因爲允許程序打破死鎖。
- 也可以調用 lockInterruptibly 方法。它就相當於一個超時設爲無限的 tryLock 方法。
- 在等待一個條件時,也可以提供一個超時:
如果一個線程被另一個線程通過調用 signalAll 或 signal 激活,或者超時時限已達到,或者線程被中斷,那麼 await 方法將返回。myCondition.await(100,TimeUnit.MILLISECONDS)
如果等待的線程被中斷, await 方法將拋出一個 InterruptedException 異常。在你希望出現這種情況時線程繼續等待(可能不太合理),可以使用 awaitUniterruptibly 方法代替 await 。 - java.util.concurrent.locks.Lock 5.0
- boolean tryLock()
嘗試獲得鎖而沒有發生阻塞;如果成功返回真。這個方法會搶奪可用的鎖,即使該鎖有公平加鎖策略,即便其他線程已經等待很久也是如此。 - boolean tryLock(long time,TimeUnit unit)
嘗試獲得鎖,阻塞時間不會超過給定的值;如果成功返回 true 。 - void lockInterruptibly()
獲得鎖,但是會不確定地發生阻塞。如果線程被中斷,拋出一個 InterruptedException 異常。
- boolean tryLock()
- java.util.concurrent.locks.Condition 5.0
- boolean await(long time,TimeUnit unit)
進入該條件的等待集,直到線程從等待集中移出或等待了指定的時間之後才解除阻塞。。如果因爲等待時間到了而返回就返回 false ,否則返回 true 。 - void awaitUninterruptinly()
進入該條件的等待集,直到線程從等待集移出才解除阻塞。如果線程被中斷,該方法不會拋出 InterruptedException 異常。
- boolean await(long time,TimeUnit unit)
14.5.14 讀/寫鎖
- java.util.concurrent.locks包定義了兩個鎖類, ReentrantLock 類和 ReentrantReadWriteLock 類。如果很多線程從一個數據結構讀取數據而很少線程修改其中數據的話,後者是十分有用的。在這種情況下,允許對讀者共享訪問是合適的。當然,寫者線程依然必須是互斥訪問的。
- 使用讀/寫鎖的必要步驟:
(1)構造一個ReentrantReadWriteLock對象:
(2)抽取讀鎖和寫鎖:private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
(3)對所有的獲取方法加讀鎖:private Lock readLock = rwl.readLock(); private Lock writeLock = rwl.writeLock();
(4)對所有的修改方法加寫鎖:public double getTotalBalance() { readLock.lock(); try{...} finally{readLock.unlock();} }
public void transfer(...) { writeLock.lock(); try{...} finally{writeLock.unlock();} }
- java.util.concurrent.locks.ReentrantReadWriteLock 5.0
- Lock readLock()
得到一個可以被多個讀操作共用的讀鎖,但會排斥所有寫操作。 - Lock writeLock()
得到一個寫鎖,排斥所有其他的讀操作和寫操作。
- Lock readLock()
14.5.15 爲什麼棄用stop和suspend方法
- 初始的Java版本定義了一個 stop 方法用來終止一個線程,以及一個 suspend 方法來阻塞一個線程直至另一個線程調用 resume 。 stop 和 suspend 方法有一些共同點:都試圖控制一個給定線程的行爲。
這兩個方法已經棄用。 stop 方法天生就不安全,經驗證明 suspend 方法會經常導致死鎖。 - stop 方法終止所有未結束的方法,包括 run 方法。當線程被終止,立即釋放被它鎖住的所有對象的鎖。這會導致對象處於不一致的狀態。
當線程要終止另一個線程時,無法知道什麼時候調用 stop 方法是安全的,什麼時候導致對象被破壞。因此,該方法被棄用了。在希望停止線程的時候應該中斷線程,被中斷的線程會在安全的時候停止。
一些作者聲稱 stop 方法被棄用是因爲它會導致對象被一個已停止的線程永久鎖定。但是,這一說法是錯誤的。從技術上講,被停止的線程通過拋出 ThreadDeath 異常退出所有它所調用的同步方法。結果是,該線程釋放它持有的內部對象鎖。 - 與 stop 不同, suspend 不會破壞對象。但是,如果用 suspend 方法的線程試圖獲得同一個鎖,那麼,該鎖在恢復之前是不可用的。如果調用 suspend 方法的線程試圖獲得同一個鎖,那麼程序死鎖:被掛起的線程等着被恢復,而將其掛起的線程等待獲得鎖。
- 如果想安全地掛起線程,引入一個變量 suspendRequested 並在 run 方法的某個安全的地方測試它,安全的地方是指該線程沒有封鎖其他線程需要的對象的地方。當該線程發現 suspendRequested 變量已經設置,將會保持等待狀態直到它再次獲得爲止。
14.6 阻塞隊列
- 對於許多線程問題,可以通過使用一個或多個隊列以優雅且安全的方式將其形式化。
- 當試圖向隊列添加元素而隊列已滿,或是想從隊列移除元素而隊列爲空的時候,阻塞隊列( bolcking queue )導致線程阻塞。在協調多個線程之前的合作時,阻塞隊列是一個有用的工具。工作者線程可以週期性地將中間結果存儲在阻塞隊列中。其他的工作者線程移出中間結果並進一步加以修改。隊列會自動的平衡負載。如果第一個線程集運行得比第二個慢,第二個線程集在等待結果時會阻塞。如果第一個線程集運行得快,它將等待第二個隊列集趕上來。
- 阻塞隊列方法
方法 正常動作 特殊情況下的動作 add 添加一個元素 如果隊列滿,則拋出IllegalStateException異常 element 返回隊列的頭元素 如果隊列空,拋出NoSuchElementException異常 offer 添加一個元素並返回true 如果隊列滿,返回false peek 返回隊列的頭元素 如果隊列空,則返回null poll 移出並返回隊列的頭元素 如果隊列空,則返回null put 添加一個元素 如果隊列滿,則阻塞 remove 移出並返回頭元素 如果隊列空,則拋出NoSuchElementException異常 take 移出並返回頭元素 如果隊列空,則阻塞 - 阻塞隊列方法分爲以下 3 類,這取決於當隊列滿或空時它們的響應方式。如果將隊列當作線程管理工具來使用,將要用到 put 和 take 方法。當試圖向滿的隊列中添加或從空的隊列中移出元素時, add 、remove和element操作拋出異常。當然,在一個多線程程序中,隊列會在任何時候空或滿,因此,一定要使用offer、poll和peek方法作爲替代。這些方法如果不能完成任務,只是給出一個錯誤提示而不會拋出異常。
- poll和peek方法返回空來指示失敗。因此,向這些隊列中插入null值是非法的。
- 還有帶有超市的offer方法和poll方法的變體。例如,下面的調用:
嘗試在100毫秒的時間內在隊列的尾部插入一個元素。如果成功返回true;否則,達到超時時,返回false。類似地,下面的調用:boolean success = q.offer(x,100,TimeUnit.MILLISECONDS);
嘗試用100毫秒的時間移除隊列的頭元素;如果成功返回頭元素,否則,達到在超時時,返回null。Object head = q.poll(100,TimeUnit.MILLISEDS);
如果隊列滿,則put方法阻塞;如果隊列空,則take方法阻塞。在不帶超時參數時,offer和poll方法等效。 - java.util.concurrent包提供了阻塞隊列的幾個變種。默認情況下,LinkedBlockingQueue的容量是沒有上邊界的,但是,也可以選擇指定最大容量。LinkedBlockingDeque是一個雙端的版本。ArrayBlockingQueue在構造時需要指定容量,並且有一個可選的參數來指定是否需要公平性。若設置了公平參數,則那麼等待了最長時間的線程會優先得到處理。通常,公平性會降低性能,只有在確實非常需要時才使用它。
- PriorityBlockingQueue是一個帶優先級的隊列,而不是先進先出隊列。元素按照它們的優先級順序被移出。該隊列是沒有容量上限,但是,如果隊列是空的,取元素的操作會阻塞。
- DelayQueue包含是Delayed接口的對象:
getDelay方法返回對象的殘留延遲。負值表示延遲已經結束。元素只有在延遲用完的情況下才能從DelayQueue移除。還必須實現compareTo方法。DelayQueue使用該方法對元素進行排序。interface Delayed extends Comparable< Delayed > { long getDelay(TimeUnit unit); }
- Java SE 7增加了一個TransferQueue接口,允許生產者線程等待,直到消費者準備就緒可以接收一個元素。如果生產者調用
這個調用會阻塞,直到另一個線程將元素(item)刪除。LinkedTransferQueue實現了這個接口。q.transfer(item);
- java.util.concurrent.ArrayBlockingQueue< E > 5.0
- ArrayBlockingQueue(int capacity)
- ArrayBlockingQueue(int capacity,boolean fair)
構造一個帶有指定的容量和公平性的阻塞隊列。該隊列用循環數組實現。
- java.util.concurrent.LinkedBlockingQueue< E > 5.0
java.uti..concurrent.LinkedBlockingDeque< E > 6.0- LinkedBlockingQueue()
- LinkedBlockingDeque()
構造一個無上限的阻塞隊列或雙向隊列,用鏈表實現。 - LinkedBolckingQueue(int capacity)
- LinkedBlockingDeque(int capacity)
根據指定容量構建一個有限的阻塞隊列或雙向隊列,用鏈表實現。
- java.util.concurrent.DelayQueue< E extends Delayed > 5.0
- DelayQueue()
構造一個包含Delayed元素的無界的阻塞時間有限的阻塞隊列。只有那些延遲已經超過時間的元素可以從隊列中移出。
- DelayQueue()
- java.util.concurrent.Delayed 5.0
- long getDelay(TimeUnit unit)
得到該對象的延遲,用給定的時間單位進行度量。
- long getDelay(TimeUnit unit)
- java.util.concurrent.PriorityBlockingQueue< E > 5.0
- PriorityBlockingQueue()
- PriorityBlockingQueue(int initialCapacity)
- PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator)
構造一個無邊界阻塞優先隊列,用堆實現。
參數:initialCapacity 優先隊列的初始容量。默認值是11。
comparator 用來對元素進行比較的比較器,如果沒有指定,則元素必須實現Comparable接口。
- java.util.concurrent.BlockingQueue< E > 5.0
- void put(E element)
添加元素,在必要時阻塞。 - E take()
移除並返回頭元素,必要時阻塞。 - boolean offer(E element,long time,TimeUnit unit)
添加給定的元素,如果成功返回true,如果必要時阻塞,直至元素已經被添加或超時。 - E poll(long time,TimeUnit unit)
移除並返回頭元素,必要時阻塞,直至元素可用或超時用完。失敗時返回null。
- void put(E element)
- java.util.concurrent.BolckingDeque< E > 6
- void putFirst(E element)
- void putLast(E element)
添加元素,必要時阻塞。 - E takeFirst()
- E takeLast()
移除並但會頭元素或尾元素,必要時阻塞。 - boolean offerFirst(E element,long time,TimeUnit unit)
- boolean offerLast(E element,long time,TimeUnit unit)
添加給定的元素,成功時返回true,必要時阻塞直至元素被添加或超時。 - E pollFirst(long time,TimeUnit unit)
- E pollLast(long time,TimeUnit unit)
移動並返回頭元素或尾元素,必要時阻塞,直至元素可用或超時。失敗時返回null。
- java.util.concurrent.TransferQueue< E > 7
- void transfer(E element)
- boolean tryTransfer(E element,long time,TimeUnit unit)
傳輸一個值,或者嘗試在給定的超時時間內傳輸這個值,這個調用將阻塞,直到另一個線程將元素刪除。第二個方法會在調用成功時返回true。
14.7 線程安全的集合
- 可以通過提供鎖來保護共享數據結構,但是選擇線程安全的實現作爲替代可能更容易寫。
14.7.1 高效的映射表、集合和隊列
- java.util.concurrent包提供了映射表、有序集合隊列的高效實現:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。這些集合使用複雜的算法,通過允許併發地訪問數據結構的不同部分來使競爭極小化。
- 與大多數集合不同,size方法不必在常量時間內操作。確定這樣的集合當前的大小通常需要遍歷。
- 集合返回弱一致性(weakly consisteut)的迭代器。這意味着迭代器不一定能反映出它們被構造之後的所有的修改,但是,它們不會將同一個值返回兩次,也不會拋出Concurrent ModificationException異常。
- 與之形成對照的是,集合如果在迭代器構造之後發生改變,java.util包中的迭代器將拋出一個ConcurrentModificationException異常。
- 併發地散列映射表,可高效地支持大量的讀者和一定數量的寫者。默認情況下,假定可以有多達16個寫者線程同時執行。可以有更多的寫者線程,但是,如果同一時間多於16個,其他線程將暫時被阻塞。可以指定更大數目的構造器,然而,沒有這種必要。
- ConcurrentHashMap和ConcurrentSkipListMap類有相應的方法用於原子性的關聯插入以及關聯刪除。putIfAbsent方法自動地添加新的關聯,前提是原來沒有這一關聯。對於多線程訪問的緩存來說這是很有用的,確保只有一個線程向緩存添加項:
相反的操作是刪除(或許應該叫做removeIfPresent)。調用cache.putIfAbsent(key,value);
將原子性地刪除鍵值對,如果它們在映像表中出現的話。最後,cache.remove(key,value);
原子性地用新值替換舊值,假定舊值與指定的鍵值關聯。cache.replace(key,oldValue,newValue);
- java.util.concurrent.ConcurrentLinkedQueue< E > 5.0
- ConcurrentLinkedQueue< E >()
構造一個可以被多線程安全訪問的無邊界非阻塞的隊列。
- ConcurrentLinkedQueue< E >()
- java.util.concurrent.ConcurrentLinkedQueue< E > 5.0
- ConcurrentSkipListSet< E >()
- ConcurrentSkipListSet< E >(Comparator<? super E> comp)
構造一個可以被多線程安全訪問的有序集。第一個構造器要求元素實現Comparable接口。
- java.util.concurrent.ConcurrentHashMap< K,V > 5.0
java.util.concurrent.ConcurrentSkipListMap< K,V > 6- ConcurrentHashMap< K,V >()
- ConcurrentHashMap< K,V >(int initialCapacity)
- ConcurrentHashMap< K,V >(int initialCapacity,float loadFactor,int concurrencyLevel)
構造一個可以被多線程安全訪問的散列映射表。
參數:initialCapacity 集合的初始容量。默認值爲16。
loadFactor 控制調整:如果每一個桶的平均負載超過這個因子,表的大小會被重新調整。默認值是0.75。
concurrencyLevel 併發寫者線程的估計數目。 - ConcurrentSkipListMap< K,V >()
- ConcurrentSkipListSet< K,V >(Comparator<? super K> comp)
構造一個可以被多線程安全訪問的有序的映像表。第一個構造器要求鍵實現Comparable接口。 - V putIfAbsent(K key,V value)
如果該鍵沒有在映像表中出現,則將給定的值同給定的鍵關聯起來,並返回null。否則返回與該鍵關聯的現有值。 - boolean remove(K hey,V value)
如果給定的鍵與給定的值關聯,刪除給定的鍵與值並返回真。否則,返回false。 - boolean replace(K key,V oldValue,V newValue)
如果給定的鍵當前與oldvalue相關聯,用它與newValue關聯。否則,返回false。
14.7.2 寫數組的拷貝
- CopyOnWriteArrayList和CopyOnWriteArraySet是線程安全的集合,其中所有的修改線程對底層數組進行復制。如果在集合上進行迭代的線程數超過修改線程數,這樣的安排是很有用的。當構建一個迭代器的時候,它包含一個對當前數組的引用。如果數組後來被修改了,迭代器仍然引用舊數組,但是,集合的數組已經被替換了。因此,舊的迭代器擁有一致的(可能過時的)視圖,訪問它無須同步開銷。
14.7.3 較早的線程安全集合
- 從Java的初始版本開始,Vector和Hashtable類就提供了線程安全的動態數組和散列表的實現。現在這些類被棄用了,取而代之的是ArrayList和HashMap類。這些類不是線程安全的。而集合庫中提供了不同的機制。任何集合類通過使用同步包裝器(synchronization wrapper)變成線程安全的:
List< E > synchArrayList = Collections.synchronizedList(new ArrayList< E >()); Map< K,V > synchHashMap = Collections.synchronizedMap(new HashMap< K,V >());
- 結果集合的方法使用鎖加以保護,提供了線程的安全訪問。
- 應該確保沒有任何線程通過原始的非同步方法訪問數據結構。最便利的方法是確保不保存任何指向原始對象的引用,簡單地構造一個集合並立即傳遞給包裝器。
- 如果在另一個線程可能進行修改時要對集合進行迭代,仍然需要使用“客戶端”鎖定:
synchronized(synchHashMap) { Iterator< K > iter = synchHashMap.keySet().iterator(); while(iter.hashNext())...; }
- 如果使用“for each”循環必須使用同樣的代碼,因爲循環使用了迭代器。注意:如果在迭代過程中,別的線程修改集合,迭代器會失效,拋出ConcurrentModificationException異常。同步仍然是需要的,因此併發的修改可以被可靠地檢測出來。
- 最好使用java.util.concurrent包中的集合,不使用同步包裝器中的。特別是,加入它們訪問的是不同的桶,由於ConcurrentHashMap已經精心地實現了,多線程可以訪問它而且不會彼此阻塞。有一個例外是經常被修改的數組列表。在那種情況下,同步的ArrayList可以勝過CopyOnWriteArrayList。
- java.util.collections 1.2
- static < E > Collection< E > synchronizedCollection(Collection< E > c)
- static < E > List synchronizedList(List< E > c)
- static < E > Set synchronizedSet(Set< E > c)
- static < E > SortedSet synchronizedSortedSet(SortedSet< E > c)
- static < K,V > Map< K,V > synchronizedMap(Map< K,V > c)
- static < K,V > SortedMap< K,V > synchronizedSortedMap(SortedMap< K,V > c)
構造集合視圖,該集合的方法是同步的。
14.8 Callable與Future
- Runnable封裝一個異步運行的任務,可以把它想想成爲一個沒有參數和返回值的異步方法。Callable與Runnable類似,但是有返回值。Callable接口是一個參數化的類型,只有一個方法call。
類型參數是返回值的類型。例如,Callable< Integer >表示一個最終返回Integer對象的異步計算。public interface Callable< V > { V call() throws Exception; }
- Future保存異步計算的結果。可以啓動一個計算,將Future對象交給某個線程,然後忘掉它。Future對象的所有者在結果計算好之後就可以獲得它。
- Future接口具有下面的方法:
第一個get方法的調用被阻塞,直到計算完成。如果在計算完成之前,第二個方法的調用超時,拋出一個TimeoutException異常。如果運行該計算的線程被中斷,兩個方法都將拋出InterruptedException。如果計算已經完成,那麼get方法立即返回。public interface Future< V > { V get() thros ...; V get(long timeout,TimeUnit unit) throwa...; void cancel(boolean mayInterupt); boolean isCancelled(); boolean isDone(); }
如果計算還在進行,isDone方法返回false;如果完成了,則返回true。
可以用cancel方法取消該計算。如果計算還沒有開始,它被取消且不再開始。如果計算處於運行之中,那麼如果mayInterrupt參數爲true,它就被中斷。 - FutureTask包裝器是一種非常便利的機制,可將Callable轉換成Future和Runnable,它同時實現二者的接口。
- java.util.concurrent.Callable< V > 5.0
- V call()
運行一個將產生結果的任務。
- V call()
- java.util.concurrent.Future< V > 5.0
- V get()
- V get(long time,TimeUnit unit)
獲取結果,如果沒有結果可用,則阻塞直到得到結果超過指定的事件爲止。如果不成功,第二個方法會拋出TimeoutException異常。 - boolean cancel(boolean mayInterrupt)
嘗試取消這一任務的運行。如果任務已經開始,並且mayInterrupt參數值爲true,它就會被中斷。如果成功執行了取消操作,返回true。 - boolean isCancelled()
如果任務在完成前被取消了,則返回true。 - boolean isDone()
如果任務結束,無論是正常結束、中途取消或發生異常,都返回true。
- java.util.concurrent.FutureTask< V > 5.0
- FutureTask(Callable< V > task)
- FutureTask(Runnable task,V result)
構造一個既是Future< V >又是Runnable的對象。
14.9 執行器
- 構造一個新的線程是由一定代價的,因爲涉及與操作系統的交互。如果程序中創建了大量的生命週期很短的線程,應該使用線程池(thread pool)。一個線程池中包含許多準備運行的空閒線程。將Runnable對象交給線程池,就會有一個線程調用run方法。當run方法退出時,線程不會死亡,而是在池中準備爲下一個請求提供服務。
- 另一個使用線程池的理由是減少併發線程的數目。創建大量線程會大大降低性能甚至使虛擬機崩潰。如果有一個會創建許多線程的算法,應該使用線程數“固定的”線程池以限制併發線程的總數。
- 執行器(Executor)類有許多靜態工廠方法用來構建線程池。
- 執行者工廠方法
方法 描述 newCachedThreadPool 必要時創建新線程;空閒線程會被保留60秒 newFixedThreadPool 該池包含固定數量的線程;空閒線程會一直被保留 newSingleThreadExecutor 只有一個線程的“池”,該線程順序執行每一個提交的任務(類似於Swing事件分配線程) newScheduledThreadPool 用於預定執行而構建的固定線程池,替代java.util.Timer newSingleThreadScheduleExecutor 用於預定執行而構建的單線程“池”
14.9.1 線程池
- newCachedThreadPool方法構建了一個線程池。對於每個任務,如果有空閒線程可用,立即讓它執行任務,如果沒有可用的空閒線程,則構建一個新線程。newFixedThreadPool方法構建一個具有固定大小的線程池。如果提交的任務數多於空閒的線程數,那麼把得不到服務的任務放置到隊列中。當其他任務完成以後再運行它們。newSingleThreadExecutor是一個退化了的大小爲1的線程池:由一個線程執行提交的任務,一個接着一個。這3個方法返回實現了ExecutorService接口的ThreadPoolExecutor類的對象。
- 可用下面的方法之一將一個Runnable對象或Callable對象提交給ExecutorService:
該池會在方便的時候儘早執行提交的任務。調用submit時,會得到一個Future對象,可用來查詢該任務的狀態。Future<?> submit(Runnable task) Future< T > submit(Runnable task,T result) Future< T > submit(Callable< T > task)
第一個submit方法返回一個奇怪樣子的Future<?>。可以使用這樣一個對象來調用isDone、cancel或isCancelled。但是,get方法在完成的時候只是簡單地返回null。
第二個版本的Submit也提交一個Runnable,並且Future的get方法在完成的時候返回指定的result對象。
第三個版本的Submit提交一個Callable,並且返回的Future對象將在計算結果準備好的時候得到它。 - 當用完一個線程池的時候,調用shutdown。該方法啓動該池的關閉序列。被關閉的執行器不再接受新的任務。當所有任務都完成以後,線程池中的線程死亡。另一個方法是調用shutdownNow。該池取消尚未開始的所有任務並試圖中斷正在運行的線程。
- 下面總結了在使用連接池時應該做的事:
(1)調用Executors類中靜態的方法newCachedThreadPool或newFixedThreadPool。
(2)調用submit提交Runnable或Callable對象。
(3)如果想要取消一個任務,或如果提交Callable對象,那就要保存好返回的Future對象。
(4)當不再提交任何任務時,調用shutdown。 - java.util.concurrent.Executors 5.0
- ExecutorService newCachedThreadPool()
返回一個帶緩存的線程池,該池在必要的時候創建線程,在線程空閒60秒之後終止線程。 - ExecutorService newFixedThreadPool(int threads)
返回一個線程池,該池中的線程數由參數指定。 - ExecutorService newSingleThreadExecutor()
返回一個執行器,它在一個單個的線程中一次執行各個任務。
- ExecutorService newCachedThreadPool()
- java.util.concurrent.ExecutorService 5.0
- Future< T > submit(Callable< T > task)
- Future< T > submit(Runnable task,T result)
- Future< ? > submit(Runnable task)
提交指定的任務去執行。 - void shutdown()
關閉服務,會先按成已經提交的任務而不再接收新的任務。
- java.util.concurrent.ThreadPoolExecutor 5.0
- int getLargestPoolSize()
返回線程池在該執行器盛行週期中的最大尺寸。
- int getLargestPoolSize()
14.9.2 預定執行
- ScheduleExecutorService接口具有爲預定執行(Scheduled Execution)或重複執行任務而設計的方法。它是一種允許使用線程池機制的java.util.Timer的泛化。Executors類的newScheduledThreadPool和newSingleThreadScheduledExecutor方法將返回實現了ScheduledExecutorService接口的對象。
- 可以預定Runnable或Callable在初始的延遲之後只運行一次。也可以預定一個Runnable對象週期性地運行。
- java.util.concurrent.Executors 5.0
- ScheduledExcutorService newScheduledThreadPool(int threads)
返回一個線程池,它使用給定的線程數來調度任務。 - ScheduledExecutorService newSingleThreadScheduledExecutor()
返回一個執行器,它在一個單獨線程中調度任務。
- ScheduledExcutorService newScheduledThreadPool(int threads)
- java.util.concurrent.ScheduledExecutorService 5.0
- ScheduledFuture< V > schedule(Callable< V > task,long time,TimeUnit unit)
- ScheduledFuture< ? > schedule(Runnable task,long time,TimeUnit unit)
預定在指定的時間之後執行任務。 - ScheduledFuture<?> scheduleAtFixedRate(Runnable task,long initialDelay,long period,TimeUnit unit)
預定在初始的延遲結束後,週期性地運行給定的任務,週期長度是period。 - ScheduledFuture<?> scheduleWithFixedDelay(Runnable task,long initialDelay,long delay,TimeUnit unit)
預定在初始的延遲結束後週期性地給定的任務,再一次調用完成和下一次調用開始之間有長度爲delay的延遲。
14.9.3 控制任務組
- invokeAny方法提交所有對象到一個Callable對象的集合中,並返回某個已經完成了的任務的結果。無法知道返回的究竟是哪個任務的結果,也許是最先完成的那個任務的結果。對於搜素問題,如果你願意接受任何一種解決方案的話,就可以使用這個方法。
- invokeAll方法提交所有對象到一個Callable對象的集合中,並返回一個Future對象的列表,代表所有任務的解決方案。當計算結果可獲得時,可以像下面這樣對結果進行處理:
這個方法的缺點是如果第一個任務恰巧花去了很多時間,則可能按可獲得的順序保存起來更有實際意義。可以用ExecutorCompletionService來進行排列。List<Callable< T >> tasks=...; List<Future< T >> results = executor.invokeAll(tasks); for (Future< T > result:results) processFurther(result.get());
- 用常規的方法獲得一個執行器。然後,構建一個ExecutorCompletionService,提交任務給完成服務(completion service)。該服務管理Future對象的阻塞隊列,其中包含已經提交的任務的執行結果(當這些結果成爲可用時)。這樣一來,相比前面的計算,一個更有效的組織形式如下:
ExecutorCompletionService service = new ExecutorCompletionService(executor); for(Callable< T > task:tasks) service.submit(task); for (int i=0;i<task.size();i++) processFurther(service.task().get());
- java.util.concurrent.ExecutorService 5.0
- T invokeAny(Collection< Callable< T >> tasks)
- T invokeAny(Collection< Callable< T >> tasks,long timeout,TimeUnit unit)
執行給定的任務,返回其中一個任務的結果。第二個方法若發生超時,拋出一個Timeout Exception異常。 - List< Future< T >> invokeAll(Collection<callable< t="">> tasks)
- List< Future< T >> invokeAll(Collection< Callable< T >> tasks,long timeout,TimeUnit unit)
執行給定的任務,返回所有執行的結果。第二個方法若發生超時,拋出一個TimeOutEException超時。
- java.util.concurrent.ExecutorCompletionService 5.0
- ExecutorCompletionService(Executor e)
構造一個執行器完成服務來收集給定執行器的結果。 - Future< T > submit(Callable< T > task)
- Future< T > submit(Runnable task,T result)
提交一個任務給底層的執行器。 - Future< T > task()
移除下一個已完成的結果,如果沒有任何已完成的結果可用則阻塞。 - Future< T > poll()
- Future< T > poll(long time,TimeUnit unit)
移除下一個已完成的結果,如果沒有任何已完成結果可用則返回null。第二個方法將等待給定的時間。
- ExecutorCompletionService(Executor e)
14.9.4 Fork-Join框架
- 有些應用使用了大量線程,但其中大多數都是空閒的。舉例來說,一個Web服務器可能會爲每個連接分別使用一個線程。另外一些應用可能對每個處理器內核分別使用一個線程,來完成計算密集型任務,如圖像或視頻處理。Java SE 7中新引入了fork-join框架,專門用來支持後一類應用。假設有一個處理任務,它可以很自然地分解爲子任務,如下所示:
if(problemSize > threshold) solve problem direckly else { break problem into subproblems recursively solve each subproblem combine the results }
- 在後臺,fork-join框架使用了一種有效的智能方法來平衡可用線程的工作負載,這種方法稱爲工作密取(work stealing)。每個工作線程都有一個雙端隊列(deque)來完成任務。一個工作線程將子任務壓入其雙端隊列的隊頭。(只有一個線程可以訪問隊頭,所以不需要加鎖。)一個工作線程空閒時,它會從另一個雙端隊列的隊尾“密取”一個任務。由於大的子任務都在隊尾,這種密取很少出現。
14.10 同步器
- java.util.concurrent包包含了幾個能幫助人們管理相互合作的線程集的類見下表。這些機制具有爲線程之間的共用集結點模式(common rendezous patterns)提供的“預置功能”(canned functionality)。如果有一個相互合作的線程集滿足這些行爲模式之一,那麼應該直接重用合適的庫類而不要試圖提供手工的鎖與條件的集合。
同步器類 它能做什麼 何時使用 CyclicBarrier 允許線程集等待直至其中預定數目的線程到達一個公共障柵(barrier),然後可以選擇執行一個處理障柵的動作 當大量的線程需要在它們的結果可用之前完成時 CountDownLatch 允許線程集等待直到計數器減爲0 當一個或多個線程需要等待直到指定數目的事件發生 Exchanger 允許兩個線程在要交換的對象準備好時交換對象 當兩個線程工作在同一個數據結構的兩個實例上的時候,一個向實例添加數據而另一個從實例清除數據 Semaphore 允許線程集等待直到被允許繼續運行爲止 限制訪問資源的線程總數。如果許可數是1,常常阻塞線程直到另一個線程給出許可爲止 SynchronoutQueue 允許一個線程把對象交給另一個線程 在沒有顯式同步的情況下,當兩個線程準備好將一個對象從一個線程傳遞到另一個時
14.10.1 信號量
- 概念上講,一個信號量管理許多的許可證( permits )。爲了通過信號量,線程通過調用 acquire 請求許可。其實沒有實際的許可對象,信號量僅維護一個計數。許可的數目是固定的,由此限制了通過的線程數量。其他線程可以通過調用 release 釋放許可。而且,許可不是必須由獲取它的線程釋放。事實上,任何線程都可以釋放任意數目的許可,這可能會增加許可數目以至於超出初始數目。
- 信號量在 1968 年由 Edsger Dijkstra 發明,作爲同步原語( synchronization primitive )。 Dijkstra指出信號量可以被有效地實現,並且有足夠的能力解決許多常見的線程同步問題。在幾乎任何一本操作系統教科書中,都能看到使用信號量實現的有界隊列。
應用程序員不必自己實現有界隊列。通常,信號量不必直接映射到通用應用場景。
14.10.2 倒計時門栓
- 一個倒計時門栓( CountDownLatch )讓一個線程集等待直到計數變爲 0 。倒計時門栓是一次性的。一旦計數爲 0 ,就不能再重用了。
- 一個有用的特例是計數值爲 1 的門栓。實現一個只能通過一次的門。線程在門外等候直到另一個線程將計數器值置爲 0 。
14.10.3 障柵
- CyclicBarrier 類實現了一個集結點( rendezvous )稱爲障柵( barrier )。考慮大量線程運行在一次計算的不同部分的情形。當所有部分都準備好時,需要把結果組合在一起。當一個線程完成了它的那部分任務後,我們讓它運行到障柵處。一旦所有的線程都到達了這個障柵,障柵就撤銷,線程就可以繼續運行。
- 如果任何一個在障柵上等待的線程離開了障柵,那麼障柵就被破壞了(線程可能離開是因爲它調用 await 時設置了超時,或者因爲它被中斷了)。在這種情況下,所有其他線程的 await 方法拋出 BrokenBarrierException 異常。那些已經在等待的線程立即終止 await 的調用。
- 障柵被稱爲是循環的( cyclic ),因爲可以在所有等待線程被釋放後被重用。在這一點上,有別於 CountDownLatch , CountDownLatch 只能被運行一次。
- Phaser 類增加了更大的靈活性,云溪改變不同階段中參與線程的個數。
14.10.4 交換器
- 當兩個線程在同一個數據緩衝區的兩個實例上工作的時候,就可以使用變換器( Exchanger )。典型的情況是,一個線程想緩衝區填入數據,另一個線程消耗這些數據。當它們都完成以後,相互交換緩衝區。
14.10.5 同步隊列
- 同步隊列是一種將生產者與消費者線程配對的機制。當一個線程調用 SynchronousQueue 的 put 方法時,它會阻塞直到另一個線程調用 take 方法爲止,反之亦然。與 Exchanger 的情況不同,數據僅僅沿一個方向傳遞,從生產者到消費者。
- 即使 SynchronousQueue 類實現了 BlockingQueue 接口,概念上講,它依然不是一個隊列。它沒有包含任何元素,它的 size 方法總是返回 0 。
14.11 線程與Swing
- 在程序中使用線程的理由之一是提高程序的響應性能。當程序需要做某些耗時的工作時,應該啓動另一個工作器線程而不是阻塞用戶接口。
- 必須認真考慮工作器線程在做什麼,因爲這或許令人驚訝, Swing 不是線程安全的。如果試圖在多個縣城中操縱用戶界面的元素,那麼用戶界面可能奔潰。
14.11.1 運行耗時的任務
-
將線程與 Swing 一起使用時,必須遵循兩個簡單的原則。
- 如果一個動作需要花費很長時間,在一個獨立的工作器線程中做這件事不要在事件分配線程中做。
- 除了事件分配線程,不要在任何線程中接觸 Swing 組件。
制定第一條規則的理由易於理解。如果花很多時間在事件分配線程上,應用程序像“死了”一樣,因爲它不響應任何事件。特別是,事件分配線程應該永遠不要進行 input/output 調用,這有可能會阻塞,並且永遠不要調用 sleep 。(如果需要等待指定的時間,使用定時器事件。)
第二條規則在 Swing 編程中通常稱爲單一線程規則( single-thread rule )。
這兩條規則看起來彼此衝突。假定要啓動一個獨立的線程運行一個耗時的任務。線程工作的時候,通常要燈芯用戶界面中指示執行的進度。任務完成的時候,要再一次更新 GUI 界面。但是,不能從自己的線程接觸 Swing 組件。例如,如果要更新進度條或標籤文本,不能從線程中設置它的值。
要解決這一問題,在任何線程中,可以使用兩種有效的方法向事件隊列添加任意的動作。例如,假定想在一個線程中週期性地更新標籤來表明進度。不可以從自己的線程中調用 label.setText ,而應該使用 EventQueue 類的 invokeLater 方法和 invokeAndWait 方法使所調用的方法在事件分配線程中執行。
應該將 Swing 代碼放置到實現 Runnable 接口的類的 run 方法中。然後,創建該類的一個對象,將其傳遞給靜態的 invokeLater 或 invokeAndWait 方法。
當事件放入事件隊列時, invokeLater 方法立即返回,而 run 方法被異步執行。 invokeAndWait 方法等待直到 run 方法確實被執行過爲止。
有更新進度標籤時, invokeLater 方法更適宜。用戶更希望讓工作器線程有更快完成工作而不是得到更加精確的進度指示器。
這兩種方法都是在事件分配線程中執行 run 方法。沒有新的線程被創建。 - java.awt.EventQueue 1.1
- static void invokeLater(Runnable runnable) 1.2
在待處理的線程被處理之後,讓 runnable 對象的 run 方法在事件分配線程中執行。 - static void invokeAndWait(Runnable runnable) 1.2
在待處理的線程被處理之後,讓 runnable 對象的 run 方法在事件分配線程中執行。該調用會阻塞,直到 run 方法終止。 - static boolean isDispatchThread() 1.2
如果執行這一方法的線程是事件分配線程,返回 true。
- static void invokeLater(Runnable runnable) 1.2
14.11.2 使用Swing工作線程
- SwingWorker 類覆蓋 doInBackground 方法來完成耗時的工作,調用 publish 來報告工作進度。這一方法在工作器線程中執行。 publish 方法使得 process 方法在事件分配線程中執行來處理進度數據。當工作完成時, done 方法在事件分配線程中被調用以便完成 UI 的更新。
每當要在工作器線程中做一些工作時,構建一個新的工作器(每一個工作器對象只能被使用一次)。然後調用 execute 方法。典型的方式是在事件分配線程中調用 execute ,但沒有這樣的需求。 - SwingWorker 類有 3 種類型作爲類型參數。 SwingWorker< T,V > 產生類型爲 T 的結果以及類型爲 V 的進度數據。
- 要取消正在進行的工作,使用 Future 接口的 cancel 方法。當該工作被取消的時候, get 方法拋出 CancellationException 異常。
- 工作器線程對 publish 的調用會導致在事件分配線程上的 process 的調用。爲了提高效率,幾個對 publish 的調用結果,可用對 process 的一次調用成批處理。 process 方法接收一個包含所有中間結果的列表< V > 。
- javax.swing.SwingWorker< T,V > 6
- abstract T doInBackground()
覆蓋這一方法來執行後臺的任務並返回這一工作的結果。 - void process(Lisy< V > data)
覆蓋這一方法來處理事件分配線程中的中間進度數據。 - void publish(V… data)
傳遞中間進度數據到事件分配線程。從 doInBackground 調用這一方法。 - void execute()
爲工作器線程的執行預定這個工作器。 - SwingWorker.StateValue getState()
得到這個工作器線程的狀態,值爲 PENDING 、 STARTED 或 DONE 之一。
- abstract T doInBackground()
14.11.3 單一線程規則
- 單一線程規則:“除了事件分配線程,不要在任何線程中接觸 Swing 組件”。
- 對於單一線程規則存在一些例外情況:
- 可在任一個線程裏添加或移除事件監聽器。當然該監聽器的方法會在事件分配線程中被觸發。
- 只有很少的 Swing 方法是線程安全的。在 API 文檔中用這樣的句子特別標明: “儘管大多數 Swing 方法不是線程安全的,但這個方法是。”在這些線程安全的方法中最有用的是:
JTextComponent.setText
JtextArea.insert
JTextArea.append
JTextArea.replaceRange
JComponent.repaint
JComponent.revalidate
- 歷史上,單一線程規則是更加隨意的。任何線程都可以構建組件,設置優先級,將它們添加到容器中,只要這些組件沒有一個是已經被實現的( realized )。如果組件可以接受 paint 事件或 validation 事件,組件被實現。一旦調用組件的 setVisible(true) 或 pack(!) 方法或者組件已經被添加到已經被實現的容器中,就出現這樣的情況。