Java核心技術卷一基礎知識-第14章-多線程-讀書筆記

第 14 章 多線程

本章內容:
* 什麼是線程
* 中斷線程
* 線程狀態
* 線程屬性
* 同步
* 阻塞隊列
* 線程安全的集合
* Collable與Future
* 執行器
* 同步器
* 線程與Swing
  1. 一般,每個任務稱爲一個線程(thread),它是線程控制的簡稱。能夠同時運行一個以上線程的程序稱爲多線程程序(multithreaded)。
  2. 多進程與多線程有哪些區別呢?本質的區別在於每一個進程擁有本身的一整套變量,而線程則共享數據。共享變量使線程之間的通訊比進程之間的通訊更有效、更容易。此外,在有些操做系統中,與進程相比較,線程更「輕量級」,建立、撤銷一個線程比啓動新進程的開銷要小得多。

14.1 什麼是線程

  1. Thread 類的靜態 sleep 方法將暫停給定的毫秒數。調用 Thread.sleep 不會建立一個新線程, sleep 是 Thread 類的靜態方法,用於暫停當前線程的活動。 sleep 方法能夠拋出一個 InterruptedException 異常。
  2. java.lang.Thread 1.0
    • static void sleep(long millis)
      休眠給定的毫秒數。
      參數:millis 休眠的毫秒數
  3. 不要調用 Thread 類或 Runnable 對象的 run 方法。直接調用 run 方法,只會執行同一個線程中的任務,並不會啓動新線程。應該調用 Thread.start 方法。這個方法將建立一個執行 run 方法的新線程。
  4. java.lang.Thread 1.0
    • Thread(Runnable target)
      構造一個新線程,用於調用給定target的run()方法。
    • void start()
      啓動這個線程,將引起調用run()方法。這個方法將當即返回,而且新線程將並行運行。
    • void run()
      調用關聯Runnable的run方法。
  5. java.lang.Runnable 1.0
    • void run()
      必須覆蓋這個方法,並在這個方法中提供所要執行的任務指令。

14.2 中斷線程

  1. 當線程的 run 方法執行方法體重最後一條語句後,並經由執行 return 語句返回時,或者出現了在方法中沒有捕獲的異常時,線程將終止。在Java的早期版本中,還有一個 stop 方法,其餘線程能夠調用它終止線程。可是,這個方法如今已經被棄用了。
  2. 有一種能夠強制線程終止的方法。然而, interrupt 方法能夠用來請求終止線程。
  3. 當對一個線程調用 interrupt 方法時,線程的中斷狀態將被置位。這是每個線程都具備的 boolean 標誌。每一個線程都應該不時地檢查這個標誌,以判斷線程是否被中斷。
  4. 調用 Thread.currentThread().isInterrputed() 方法得到當前線程的中斷狀態是否被置位。可是,若是線程被阻塞,就沒法檢測中斷狀態。這是產生 InterruptedException 異常的地方。當在一個被阻塞的線程(調用 sleep 或 wait )上調用 interrupt 方法時,阻塞調用將會被 Interrupt Exception 異常中斷(存在不能被中斷的阻塞 I/O 調用,應該考慮選擇可中斷的調用)。
  5. 沒有任何語言方面的需求要求一個被中斷的線程應該終止。中斷一個線程不過是引發它的注意。被中斷的線程能夠決定如何響應中斷。某些線程是如此重要以致於應該處理完異常後,繼續執行,而不理會中斷。可是,更普通的狀況是,線程將簡單地將中斷做爲一個終止的請求。
  6. 若是在每次工做迭代以後都調用 sleep 方法(或者其餘的可中斷方法), isInterrpted 檢測既沒有必要也沒有用處。若是在種蒜狀態被置位時調用 sleep 方法,它不會休眠。相反,它將清除這一狀態(!)並拋出 InterrputedException 。所以,若是你的循環調用 sleep ,不會檢測中斷狀態,相反,須要捕獲 InterrputedException 異常。
  7. 有兩個很是相似的方法, interrupted 和 isInterrupted 。 Interrupted 方法是一個靜態方法,它檢測當前的線程是否被中斷。並且,調用 interrupted 方法會清除該線程的中斷狀態。另外一方面, isInterrupted 方法是一個實例方法,可用來檢驗是否有線程被中斷。調用這個方法不會改變中斷狀態。
  8. java.lang.Thread 1.0
    • void interrupt()
      向線程發送中斷請求。線程的中斷狀態將設置爲 true 。若是目前該線程被一個 sleep 調用阻塞,那麼, InterruptedException 異常被拋出。
    • static boolean interrupted()
      測試當前線程(即正在執行這一命令的線程)是否被中斷。注意,這是一個靜態方法。這一調用會產生反作用-它將當前線程的中斷狀態重置爲false。
    • boolean isInterrupted()
      測試線程是否被終止。不像靜態的中斷方法,這一調用不改變線程的中斷狀態。
    • static Thread currentThread()
      返回表明當前執行線程的 Thread 對象。

14.3 線程狀態

  1. 線程能夠有以下6種狀態:
    • New (新建立)
    • Runnable (可運行)
    • Blocked (被阻塞)
    • Waiting (等待)
    • Timed waiting (計時等待)
    • Terminated (被終止)

14.3.1 新建立線程

  1. 當用new操做符建立一個新線程,如 new Thread(r) ,該線程尚未開始運行。這意味着它的狀態是 new 。當一個線程處於新建立狀態時,程序尚未開始運行線程中的代碼。在線程運行以前還有一些基本工做要作。

14.3.2 可運行線程

  1. 一旦調用 start 方法,線程處於 runnable 狀態。一個可運行的線程可能正在運行也可能沒有運行,這取決於操做系統給線程提供運行的時間。( Java 的規範說明沒有將它做爲一個單獨狀態。一個正在運行中的線程仍然處於可運行狀態。)
  2. 一旦一個線程開始運行,它沒必要始終保持運行。事實上,運行中的線程被中斷,目的是爲了讓其餘線程得到運行機會。線程調度的細節依賴於操做系統提供的服務。搶佔式調度系統給每個可運行線程一個時間片來執行任務。當時間片用完,操做系統剝奪該線程的運行權,並給另外一個線程運行機會。當選擇下一個線程時,操做系統考慮線程的優先級。
  3. 在任何給定時刻,一個可運行的線程可能正在運行也可能沒有運行(這就是爲何將這個狀態稱爲可運行而不是運行)。

14.3.3 被阻塞線程和等待線程

  1. 當線程處於被阻塞或等待狀態時,它暫時不活動。它不運行任何代碼且消耗最少資源。直到線程調度器從新激活它。細節取決於它是怎樣達到非活動狀態的。
    • 當一個線程試圖獲取一個內部的對象鎖(而不是 java.util.concurrect 庫中的鎖),而該鎖被其餘線程持有,則該線程進入阻塞狀態。當全部其餘線程釋放該鎖,而且線程調度器容許本線程持有它的時候,該線程將變成非阻塞狀態。
    • 當線程等待另外一個線程通知調度器一個條件時,它本身進入等待狀態。在調用 Object.wait 方法或 Thread.join 方法,或者是等待 java.util.concurrent 庫中的 Lock 或 Condition 時,就會出現這種狀況。實際上,被阻塞狀態與等待狀態是由很大不一樣的。
    • 有幾個方法有一個超時參數。調用它們致使線程進入計時等待( timed waiting )狀態。這一狀態將一直保持到超時期滿或者接收到適當的通知。帶有超時參數的方法有 Thread.sleep 和 Object.wait 、 Thread.join 、 Lock.tryLock 以及 Condition.awit 的計時版。
  2. 當一個線程被阻塞或等待時(或終止時),另外一個線程被調度爲運行狀態。當一個線程被從新激活(例如,由於超時期滿或成功地得到一個鎖),調度器檢查它是否具備比當前運行線程更高的優先級。若是是這樣,調度器從當前運行線程中挑選一個,剝奪其運行權,選擇一個新的線程運行。
  3. 線程狀態圖

14.3.4 被終止的線程

  1. 線程因以下兩個緣由之一而被終止:
    • 由於 run 方法正常退出而天然死亡。
    • 由於一個沒有捕獲的異常終止了 run 方法而意外死亡。
      特別是,能夠調用線程的 stop 方法殺死一個線程。該方法拋出 ThreadDeath 錯誤對象,由此殺死線程。可是, stop 方法已過期,不要在本身的代碼中調用這個方法。
  2. 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()以後調用。這一方法已過期。

14.4 線程屬性

  1. 線程的各類屬性,其中包括:線程優先級、守護線程、線程組以及處理未捕獲異常的處理器。

14.4.1 線程優先級

  1. 在 Java 程序設計語言中,每個線程有一個優先級。默認狀況下,一個線程繼承它的父線程的優先級。能夠用 setPriority 方法提升或下降任何一個線程的優先級。能夠將優先級設置爲在 MIN_PRIORITY (在 Thread 類中定義爲 1 )與 MAX_PRIORITY (定義爲 10 )之間的任何值。 NORM_PRIORITY 被定義爲 5 。
  2. 每當線程調度器有機會選擇新線程時,它首先選擇具備較高優先級的線程。可是,線程優先級是高度依賴於系統的。當虛擬機依賴於宿主機平臺的線程實現機制時, Java 線程的優先級被映射到宿主主機平臺的優先級上,優先級個數也許更多,也許更少。
    Windows 有 7 個優先級別。一些 Java 優先級將映射到相同的操做系統優先級。在 Sun 爲 Linux 提供的 Java 虛擬機,線程的優先級被忽略-全部線程具備相同的優先級。
  3. 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()
      致使當前執行線程處於讓步狀態。若是有其餘的可運行線程具備至少與此線程一樣高的優先級,那麼這些線程接下來會被調度。注意,這是一個靜態方法。

14.4.2 守護線程

  1. 能夠經過 t.setDaemon(true) 將線程轉換爲守護線程( daemon thread )。守護線程的惟一用途是爲其餘線程提供服務。計時線程就是一個例子,它定時地發送「計時器嘀嗒」信號給其餘線程或清空過期的高速緩存項的線程。當只剩下守護線程時,虛擬機就退出了,因爲若是隻剩下守護線程,就不必繼續運行程序了。
  2. 守護線程應該永遠不去訪問固有資源,如文件、數據庫,由於它會在任什麼時候候甚至在一個操做的中間發生中斷。
  3. java.lang.Threed 1.0
    • void setDaemon(boolean isDaemon)
      標識該線程爲守護線程或用戶線程。這一方法必須在線程啓動以前調用。

14.4.3 未捕獲異常處理器

  1. 線程的 run 方法不能拋出任何被檢測的異常,可是,不被檢測的異常會致使線程終止。在這種狀況下,線程就死亡了。
    可是,不須要任何 catch 子句來處理能夠被傳播的異常。相反,就在線程死亡以前,異常被傳遞到一個被用於未捕獲異常的處理器。
    該處理器必須屬於一個實現 Thread.UncaughtExceptionHandler 接口的類。這個接口只有一個方法。
    void uncaughtException(Thread t,Throwable e)
    能夠用 setUncaughtExceptionHandler 方法爲任何線程安裝一個處理器。也能夠用 Thread 類的靜態方法 setDefaultUncaughtExceptionHandler 爲全部線程安裝一個默認的處理器。替換處理器可使用日誌API發送未捕獲異常的報告到日誌文件。
    若是不安裝默認的處理器,默認的處理器爲空。可是,若是不爲獨立的線程安裝處理器,此時的處理器就是該線程的 ThreadGroup 對象。
  2. 線程組是一個能夠統一管理的線程集合。默認狀況下,建立的全部線程屬於相同的線程組,可是,也可能會創建其餘的組。如今引入了更好的特性用於線程集合的操做,因此建議不要在本身的程序中使用線程組。
  3. ThreadGroup 類實現 Thread.UncaughtExceptionHandler 接口。它的 uncaughtException 方法作以下操做:
    1)若是該線程組有父線程組,那麼父線程組的 uncaughtException 方法被調用。
    2)不然,若是 Thread.getDefaultExceptionHandler 方法返回一個非空的處理器,則調用該處理器。
    3)不然,若是 Throwable 是 ThreadDeath 的一個實例,什麼都不作。
    4)不然,線程的名字以及 Throwable 的棧跟蹤被輸出到 System.err 上。
  4. 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
      設置或獲取未捕獲異常的處理器。若是沒有安裝處理器,則將線程組對象做爲處理器。
  5. java.lang.Thread.UncaughtExceptionHandler 5.0
    • void uncaughtException(Thread t,Throwable e)
      當一個線程因未捕獲異常而終止,按規定要將客戶報告記錄到日誌中。
      參數:t 因爲未捕獲異常而終止的線程
      e 未捕獲的異常對象
  6. java.lang.ThreadGroup 1.0
    • void uncaughtException(Thread t,Throwable e)
      若是有父線程組,調用父線程組的這一方法;或者,若是 Thread 類有默認處理器,調用該處理器,不然,輸出棧蹤影到標準錯誤流上(可是,若是 e 是一個 ThreadDeath 對象,棧蹤影是被禁用的。 ThreadDeath 對象 stop 方法產生,而該方法已通過時)。

14.5 同步

  1. 若是兩個線程存取相同的對象,而且每個線程都調用了一個修改該對象狀態的方法,這樣一個狀況一般稱爲競爭條件( race condition )。

14.5.1 競爭條件的一個例子

  1. 銀行轉帳例子

14.5.2 競爭條件詳解

  1. 一條名利是由幾條指令組成的,執行它們的線程能夠在任何一條指令點上被中斷。

14.5.3 鎖對象

  1. 有兩種機制防止代碼塊受併發訪問的干擾。Java語言提供一個 synchronized 關鍵字達到這一目的,而且 Java SE 5.0 引入了 ReentrantLock 類。 synchronized 關鍵字自動提供了一個鎖以及相關的「條件」,對於大多數須要顯示鎖的狀況,這是很遍歷的。 java.util.concurrent 框架爲這些基礎機制提供獨立的類。
  2. 用 ReentrantLock 保護代碼塊的基本結構以下:
    myLock.lock(); //a ReentrantLock object
     try
     {
         critical section
     }
     finally
     {
         myLock.unlock();//make sure the lock is unlocked even if an exception is three
     }
    這一結構確保任什麼時候刻只有一個線程進入臨界區。一旦一個線程封鎖了鎖對象,其餘任何線程都沒法經過 lock 語句。當其餘線程調用 lock 時,它們被阻塞,直到第一個線程釋放鎖對象。
  3. 把解鎖操做放在 finally 子句以內是相當重要的。若是在臨界區的代碼拋出異常,鎖必須被釋放。不然,其餘線程將永遠阻塞。
  4. 若是使用鎖,就不能使用帶資源的 try 語句。首先,解鎖方法名不是 close 。不過,即便將它重命名,帶資源的 try 語句也沒法正常工做。它的首部但願聲明一個新變量。可是若是使用一個鎖,可能想使用多個線程共享的那個變量(而不是新變量)。
  5. 鎖是可重入的,由於線程能夠重複地得到已經持有的鎖。鎖保持一個持有計數( hold count )來跟蹤對 lock 方法的嵌套調用。線程在每一次調用lock都要調用 unlock 來釋放鎖。因爲這一特性,被一個鎖保護的代碼能夠調用另外一個使用相同的鎖的方法。
  6. 一般,可能想要保護需若干個操做來更新或檢查共享對象的代碼塊。要確保這些操做完成後,另外一個線程才能使用相同對象。
  7. 要留心臨界區中的代碼,不要由於異常的拋出而跳出了臨界區。若是在臨界區代碼結束以前拋出了異常, finally 子句將釋放鎖,但會使對象可能處於一種受損狀態。
  8. java.util.concurrent.locks.Lock 5.0
    • void lock()
      獲取這個鎖;若是鎖同時被另外一個線程擁有則發生阻塞。
    • void unlock()
      釋放這個鎖。
  9. java.util.concurrent.locks.ReentrantLock 5.0
    • ReentrantLock()
      構建一個能夠被用來保護臨界區的可重入鎖。
    • ReentrantLock(boolean fair)
      構建一個帶有公平策略的鎖。一個公平鎖偏心等待時間最長的線程。可是麼這一公平的保證將大大下降性能。因此,默認狀況下,鎖沒有被強制爲公平的。
  10. 聽起來公平鎖更合理一些,可是使用公平鎖比使用常規鎖要慢不少。只有當你確實瞭解本身要作什麼而且對於你要解決的問題有一個特定的理由必須使用公平鎖的時候,纔可使用公平鎖。即便使用公平鎖,也沒法確保線程調度器是公平的。若是線程調度器選擇忽略一個線程,而該線程爲了這個鎖已經等待了很長時間,那麼就沒有機會公平地處理這個鎖了。

14.5.4 條件對象

  1. 一般,線程進入臨界區,卻發如今某一條件知足以後它才能執行。要使用一個條件對象來管理那些已經得到了一個鎖可是卻不能作有用工做的線程。因爲歷史的緣由,條件對象常常被稱爲條件變量( conditional variable )。
  2. 一個鎖對象能夠有一個或多個相關的條件對象。能夠用 newCondition 方法得到一個條件對象。習慣上給每個條件對象命名爲能夠反映它鎖表達的條件的名字。若是條件不知足,調用 Condition.await() 。當前線程如今被阻塞了,並放棄了鎖。
  3. 等待得到鎖的線程和調用 await 方法的線程存在本質上的不一樣。一旦一個線程調用 await 方法,它進入該條件的等待集。當鎖可用時,該線程不能立刻解除阻塞。相反,它處於阻塞狀態,直到另外一個線程調用同一條件上的 singalAll 方法時爲止。 singalAll() 調用從新激活由於這一條件而等待的全部線程。當這些線程從等待集中移出時,它們再次成爲可運行的,調度器將再次激活它們。同時,它們將試圖從新進入該對象。一旦鎖成爲可用的,它們中的某個將從 await 調用返回,得到該鎖並從被阻塞的地方繼承執行。
    此時,線程應該再次測試該條件。因爲沒法確保該條件被知足 -signalAll 方法僅僅是通知正在等待的線程:此時有可能已經知足條件,值得再次去檢測該條件。
  4. 一般,對 await 的調用應該在以下形式的循環體中:
    while(!(ok to proceed))
         condition.await();
  5. 相當重要的是最終須要某個其餘線程調用 signalAll 方法。當一個線程調用 await 時,它沒有辦法從新激活自身。它寄但願於其餘線程。若是沒有其餘線程來從新激活等待的線程,它就永遠再也不運行了。這將致使使人不快的死鎖( deadlock )現象。若是全部其餘線程被阻塞,最後一個活動線程在解除其餘線程的阻塞狀態以前就調用 await 方法,那麼它也被阻塞。沒有任何線程能夠解除其餘線程的阻塞,那麼該程序就掛起了。
  6. 應該什麼時候調用 signalAll 呢?經驗上講,在對象的狀態有利於等待線程的方向改變時調用 signalAll 。
  7. 注意調用 signalAll 不會當即激活一個等待線程。它僅僅解除等待線程的阻塞,以便這些線程能夠在當前線程退出同步方法以後,經過競爭實現對對象的訪問。
  8. 另外一個方法 signal ,則是隨機解除等待集中某個線程的阻塞狀態。這比解除全部線程的阻塞更加有效,但也存在危險。若是隨機選擇的線程發現本身仍然不能運行,那麼它再次被阻塞。若是沒有其餘線程再次調用 signal ,那麼系統就死鎖了。
  9. 當一個線程擁有某個條件的鎖時,它僅僅能夠在該條件上調用 await 、 signalAll 或 signal 方法。
  10. java.util.concurrent.locks.Lock 5.0
    • Condition newCondition()
      返回一個與該鎖相關的條件對象。
  11. java.util.concurrent.locks.Condition 5.0
    • void await()
      將該線程放到條件的等待集中。
    • void signalAll()
      解除該條件的等待集中的全部線程的阻塞狀態。
    • void signal()
      從該條件的等待集中隨機地選擇一個線程,解除其阻塞狀態。

14.5.5 synchronized關鍵字

  1. 鎖和條件的關鍵之處:
    • 鎖用來保護代碼片斷,任什麼時候刻只能有一個線程執行被保護的代碼。
    • 鎖能夠管理試圖進入被保護代碼段的線程。
    • 鎖能夠擁有一個或多個相關的條件對象。
    • 每一個條件對象管理那些已經進入被保護的代碼段但還不能運行的線程。
  2. 從 1.0 版開始, Java 中的每個對象都有一個內部鎖。若是一個方法用 synchronized 關鍵字聲明,那麼對象的鎖將保護整個方法。也就是說,要調用該方法,線程必須得到內部的對象鎖。
  3. 內部對象鎖只有一個相關條件。 wait 方法添加一個線程到等待集中, notifyAll/notify 方法解除等待線程的阻塞狀態。換句話說,調用 wait 或 notifyAll 等價於
    intrinsicCondition.await();
     intrinsicCondition.signalAll();
  4. wait 、 notifyAll 以及 notify 方法是 Object 類的 final 方法。 Condition 方法必須被命名爲 await 、 signalAll 和 signal 以便它們不會與那些方法發生衝突。
  5. 將靜態方法聲明爲 synchronized 也是合法的。若是調用這個方法,該方法得到相關的類對象的內部類。所以,沒有其餘線程能夠調用同一個類的這個或任何其餘的同步靜態方法。
  6. 內部鎖和條件存在一些侷限。包括:
    • 不能中斷一個正在試圖得到鎖的線程。
    • 試圖得到鎖時不能設定超時。
    • 每一個鎖僅有單一的條件,多是不夠的。
  7. 在代碼中應該使用哪種? Lock 和 Conditon 對象仍是同步方法?下面是一些建議:
    • 最好既不使用 Lock/Condition 也不實用 synchronized 關鍵字。在許多狀況下你可使用 java.util.concurrent 包中的一種機制,它會爲你處理全部的加鎖。
    • 若是 synchronized 關鍵字適合你的程序,那麼請儘可能使用它。這樣能夠減小編寫的代碼數量,減小出錯的概率。
    • 若是特別須要 Lock/Condition 結構提供的特有特性時,才使用 Lock/Condition 。
  8. 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

14.5.6 同步阻塞

  1. 每個 Java 對象有一個鎖。線程能夠經過調用同步方法得到鎖。還有另外一種機制能夠得到鎖,經過進入一個同步阻塞。當線程進入以下形式的阻塞:
    synchronized(obj) //this is the     synchronized block
     {
         critical section
     }
    因而它得到 obj 的鎖。
  2. 使用一個對象的鎖來實現額外的原子操做,實際上稱爲客戶端鎖定( client-slide locking )。客戶端鎖定是很是脆弱的,一般不推薦使用。

14.5.7 監視器概念

  1. 鎖和條件是線程同步的強大工具,可是,嚴格來說,它們不是面向對象的。多年來,研究人員努力尋找一種方法,能夠在不須要程序員考慮如何加鎖的狀況下,就能夠保證多線程的安全性。最成功的解決方案之一是監視器( monitor ),這一律念最先是由 Per Brinch Hansen 和 Tony Hoare 在 20 世紀 70 年代提出的。用 Java 的術語來說,監視器具備以下特性:
    • 監視器是隻包含私有域的類。
    • 每一個監視器的對象有一個相關的鎖。
    • 使用該鎖對全部的方法進行加鎖。
    • 該鎖能夠有任意多個相關條件。
  2. Java 設計者以不是很精確的方式採用了監視器概念, Java 中的每個對象有一個內部的鎖和內部的條件。若是一個方法用 synchronized 關鍵字聲明,那麼,它表現的就像是一個監視器方法。經過調用 wait/notifyAll/notify 訪問條件變量。
  3. 在下述的 3 個方面 Java 對象不一樣於監視器,從而使得線程的安全性降低。
    • 域不要求必須是 private 。
    • 方法不要求必須是 synchronized 。
    • 內部鎖對客戶是可用的。

14.5.8 Volatile 域

  1. 有時,僅僅爲了讀寫一個或兩個實例域就使用同步,顯得開銷過大了。畢竟,什麼地方能出錯呢?遺憾的是,使用現代的處理器與編譯器,出錯的可能性很大。
    • 多處理器的計算機可以暫時在寄存器或本地內存緩衝區中保存內存中的值。結果是,運行在不一樣處理器上的線程可能在同一個內存位置取到不一樣的值。
    • 編譯器能夠改變指令執行的順序以使吞吐量最大化。這種順序上的變化不會改變代碼語義,可是編譯器假定內存的值僅僅在代碼中有顯式的修改指令時纔會改變。然而,內存的值能夠被另外一個線程改變。
      若是你使用鎖來保護能夠被多個線程訪問的代碼,那麼能夠不考慮這種問題。編譯器被要求經過在必要的時候刷新本地緩存來保持鎖的效應,而且不能不正當地從新排序指令。
  2. volatile 關鍵字爲實例域的同步訪問提供了一種免鎖機制。若是聲明一個域爲 volatile ,那麼編譯器和虛擬機就知道該域是可能被另外一個線程併發更新的。
  3. Volatile 變量不能提供原子性。例如,方法
    private volatile boolean done;
    public void flipDone(){done = !done;} //not atomic
    不能確保翻轉域中的值。

14.5.9 final 變量

  1. 除非使用域或 volatile 修飾符,不然沒法從多個線程安全地讀取一個域。還有一種狀況能夠安全地訪問一個共享域,即這個域聲明爲 final 時。考慮如下聲明:
    final Map<String,Double> accounts = new HashMap();
    其餘線程會在構造函數完成構造以後纔看到這個 accounts 變量。
    若是不使用 final ,就不能保證其餘線程看到的是 accounts 更新後的值,它們可能都只是看到 null ,而不是新構造的 HashMap 。
    對這個映射表的操做並非線程安全的。若是多個線程在讀寫這個映射表,仍然須要進行同步。

14.5.10 原子性

  1. 假設對共享變量除了賦值以外並不完成其餘操做,那麼能夠將這些共享變量聲明爲 volatic 。
  2. java.util.concurrent.atomic 包中有不少類使用了很高效的機器級指令(而不是使用鎖)來保證其餘操做的原子性。例如, AtomicInteger 類提供了方法 incrementAndGet 和 decrementAndGet ,它們分別以原子方式將一個整數自增或自減。能夠安全地使用 AtomicInteger 做爲共享計數器而無須同步。
    另外這個包中還包含 AtomicBoolean 、 AtomicLong 和 AtomicReference 以及 Boolean 值、整數、 long 值和引用的原子數組。應用程序員不該該使用這些類,它們僅供那些開發併發工具的系統程序員使用。

14.5.11 死鎖

  1. 有可能會由於每個線程要等待條件而致使全部線程都被阻塞。這樣的狀態稱爲死鎖( deadlock )。
  2. Java 編程語言中沒有任何東西能夠避免或打破這種死鎖現象,必須仔細設計程序,以確保不會出現死鎖。

14.5.12 線程局部變量

  1. 有時可能要避免共享變量,使用 ThreadLocal 輔助類爲各個線程提供各自的實例。
  2. 要爲每一個線程構造一個實例,可使用如下代碼:
    public static final ThreadLocal< SimpleDateFormat > dateFormat = new ThreadLocal< SimpleDateFomrat >()
     {
         protected SimpleDateFormat initialValue()
         {
             return new SimpleDateFormat("yyyy-MM-dd");
         }
     }
    要訪問具體的格式化方法,能夠調用:
    String dateStamp = dateFormat.get().format(new Date());
    在一個給定線程中首次調用 get 時,會調用 initilaValue 方法。在此以後, get 方法會返回屬於當前線程的那個實例。
    在多個線程中生成隨機數也存在相似的問題。 java.util.Random 類是線程安全的。可是若是多個線程須要等待一個共享的隨機數生成器,這會很低效。
    可使用 ThreadLocal 輔助類爲各個線程提供一個單獨的生成器,不過 Java SE 7 還另外提供一個便利類。只須要作一下調用:
    int random = ThreadLocalRandom.current().nextInt(upperBound);
    ThreadLocalRandom.current() 調用會返回特定於當前線程的 Random 類實例。
  3. java.lang.ThreadLocal< T > 1.2
    • T get()
      獲得這個線程的當前值。若是是首次調用get,會調用 initialize 來獲得這個值。
    • protected initialize()
      應覆蓋這個方法來提供一個初始值。默認狀況下,這個方法返回 null 。
    • void set(T t)
      爲這個線程設置一個新值。
    • void remove()
      刪除對應這個線程的值。
  4. java.util.concurrent.ThreadLocalRandom 7
    • static ThreadLocalRandom current()
      返回特定於當前線程的 Random 類實例。

14.5.13 鎖測試與超時

  1. 線程在調用 lock 方法來得到另外一個線程所持有的鎖的時候,極可能發生阻塞。應該更加謹慎地申請鎖。
  2. TimeUnit 是一個枚舉類型,能夠取的值包括 SECONDS 、 MILLISECONDS 、 MICROSECONDS 和 NANOSECONDS 。
  3. lock 方法不能被中斷。若是一個線程在等待得到一個鎖時被中斷,中斷線程在得到鎖以前一直處於阻塞狀態。若是出現死鎖,那麼, lock 方法就沒法終止。
  4. 然而,若是調用帶有用超時參數的 tryLock ,那麼若是線程在等待期間被中斷,將拋出 InterruptedException 異常。這是一個很是有用的特性,由於容許程序打破死鎖。
  5. 也能夠調用 lockInterruptibly 方法。它就至關於一個超時設爲無限的 tryLock 方法。
  6. 在等待一個條件時,也能夠提供一個超時:
    myCondition.await(100,TimeUnit.MILLISECONDS)
    若是一個線程被另外一個線程經過調用 signalAll 或 signal 激活,或者超時時限已達到,或者線程被中斷,那麼 await 方法將返回。
    若是等待的線程被中斷, await 方法將拋出一個 InterruptedException 異常。在你但願出現這種狀況時線程繼續等待(可能不太合理),可使用 awaitUniterruptibly 方法代替 await 。
  7. java.util.concurrent.locks.Lock 5.0
    • boolean tryLock()
      嘗試得到鎖而沒有發生阻塞;若是成功返回真。這個方法會搶奪可用的鎖,即便該鎖有公平加鎖策略,即使其餘線程已經等待好久也是如此。
    • boolean tryLock(long time,TimeUnit unit)
      嘗試得到鎖,阻塞時間不會超過給定的值;若是成功返回 true 。
    • void lockInterruptibly()
      得到鎖,可是會不肯定地發生阻塞。若是線程被中斷,拋出一個 InterruptedException 異常。
  8. java.util.concurrent.locks.Condition 5.0
    • boolean await(long time,TimeUnit unit)
      進入該條件的等待集,直到線程從等待集中移出或等待了指定的時間以後才解除阻塞。。若是由於等待時間到了而返回就返回 false ,不然返回 true 。
    • void awaitUninterruptinly()
      進入該條件的等待集,直到線程從等待集移出才解除阻塞。若是線程被中斷,該方法不會拋出 InterruptedException 異常。

14.5.14 讀/寫鎖

  1. java.util.concurrent.locks包定義了兩個鎖類, ReentrantLock 類和 ReentrantReadWriteLock 類。若是不少線程從一個數據結構讀取數據而不多線程修改其中數據的話,後者是十分有用的。在這種狀況下,容許對讀者共享訪問是合適的。固然,寫者線程依然必須是互斥訪問的。
  2. 使用讀/寫鎖的必要步驟:
    (1)構造一個ReentrantReadWriteLock對象:
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    (2)抽取讀鎖和寫鎖:
    private Lock readLock = rwl.readLock();
     private Lock writeLock = rwl.writeLock();
    (3)對全部的獲取方法加讀鎖:
    public double getTotalBalance()
     {
         readLock.lock();
         try{...}
         finally{readLock.unlock();}
     }
    (4)對全部的修改方法加寫鎖:
    public void transfer(...)
     {
         writeLock.lock();
         try{...}
         finally{writeLock.unlock();}
     }
  3. java.util.concurrent.locks.ReentrantReadWriteLock 5.0
    • Lock readLock()
      獲得一個能夠被多個讀操做共用的讀鎖,但會排斥全部寫操做。
    • Lock writeLock()
      獲得一個寫鎖,排斥全部其餘的讀操做和寫操做。

14.5.15 爲何棄用stop和suspend方法

  1. 初始的Java版本定義了一個 stop 方法用來終止一個線程,以及一個 suspend 方法來阻塞一個線程直至另外一個線程調用 resume 。 stop 和 suspend 方法有一些共同點:都試圖控制一個給定線程的行爲。
    這兩個方法已經棄用。 stop 方法天生就不安全,經驗證實 suspend 方法會常常致使死鎖。
  2. stop 方法終止全部未結束的方法,包括 run 方法。當線程被終止,當即釋放被它鎖住的全部對象的鎖。這會致使對象處於不一致的狀態。
    當線程要終止另外一個線程時,沒法知道何時調用 stop 方法是安全的,何時致使對象被破壞。所以,該方法被棄用了。在但願中止線程的時候應該中斷線程,被中斷的線程會在安全的時候中止。
    一些做者聲稱 stop 方法被棄用是由於它會致使對象被一個已中止的線程永久鎖定。可是,這一說法是錯誤的。從技術上講,被中止的線程經過拋出 ThreadDeath 異常退出全部它所調用的同步方法。結果是,該線程釋放它持有的內部對象鎖。
  3. 與 stop 不一樣, suspend 不會破壞對象。可是,若是用 suspend 方法的線程試圖得到同一個鎖,那麼,該鎖在恢復以前是不可用的。若是調用 suspend 方法的線程試圖得到同一個鎖,那麼程序死鎖:被掛起的線程等着被恢復,而將其掛起的線程等待得到鎖。
  4. 若是想安全地掛起線程,引入一個變量 suspendRequested 並在 run 方法的某個安全的地方測試它,安全的地方是指該線程沒有封鎖其餘線程須要的對象的地方。當該線程發現 suspendRequested 變量已經設置,將會保持等待狀態直到它再次得到爲止。

14.6 阻塞隊列

  1. 對於許多線程問題,能夠經過使用一個或多個隊列以優雅且安全的方式將其形式化。
  2. 當試圖向隊列添加元素而隊列已滿,或是想從隊列移除元素而隊列爲空的時候,阻塞隊列( bolcking queue )致使線程阻塞。在協調多個線程以前的合做時,阻塞隊列是一個有用的工具。工做者線程能夠週期性地將中間結果存儲在阻塞隊列中。其餘的工做者線程移出中間結果並進一步加以修改。隊列會自動的平衡負載。若是第一個線程集運行得比第二個慢,第二個線程集在等待結果時會阻塞。若是第一個線程集運行得快,它將等待第二個隊列集遇上來。
  3. 阻塞隊列方法
    方法 正常動做 特殊狀況下的動做
    add 添加一個元素 若是隊列滿,則拋出IllegalStateException異常
    element 返回隊列的頭元素 若是隊列空,拋出NoSuchElementException異常
    offer 添加一個元素並返回true 若是隊列滿,返回false
    peek 返回隊列的頭元素 若是隊列空,則返回null
    poll 移出並返回隊列的頭元素 若是隊列空,則返回null
    put 添加一個元素 若是隊列滿,則阻塞
    remove 移出並返回頭元素 若是隊列空,則拋出NoSuchElementException異常
    take 移出並返回頭元素 若是隊列空,則阻塞
  4. 阻塞隊列方法分爲如下 3 類,這取決於當隊列滿或空時它們的響應方式。若是將隊列看成線程管理工具來使用,將要用到 put 和 take 方法。當試圖向滿的隊列中添加或從空的隊列中移出元素時, add 、remove和element操做拋出異常。固然,在一個多線程程序中,隊列會在任什麼時候候空或滿,所以,必定要使用offer、poll和peek方法做爲替代。這些方法若是不能完成任務,只是給出一個錯誤提示而不會拋出異常。
  5. poll和peek方法返回空來指示失敗。所以,向這些隊列中插入null值是非法的。
  6. 還有帶有超市的offer方法和poll方法的變體。例如,下面的調用:
    boolean success = q.offer(x,100,TimeUnit.MILLISECONDS);
    嘗試在100毫秒的時間內在隊列的尾部插入一個元素。若是成功返回true;不然,達到超時時,返回false。相似地,下面的調用:
    Object head = q.poll(100,TimeUnit.MILLISEDS);
    嘗試用100毫秒的時間移除隊列的頭元素;若是成功返回頭元素,不然,達到在超時時,返回null。
    若是隊列滿,則put方法阻塞;若是隊列空,則take方法阻塞。在不帶超時參數時,offer和poll方法等效。
  7. java.util.concurrent包提供了阻塞隊列的幾個變種。默認狀況下,LinkedBlockingQueue的容量是沒有上邊界的,可是,也能夠選擇指定最大容量。LinkedBlockingDeque是一個雙端的版本。ArrayBlockingQueue在構造時須要指定容量,而且有一個可選的參數來指定是否須要公平性。若設置了公平參數,則那麼等待了最長時間的線程會優先獲得處理。一般,公平性會下降性能,只有在確實很是須要時才使用它。
  8. PriorityBlockingQueue是一個帶優先級的隊列,而不是先進先出隊列。元素按照它們的優先級順序被移出。該隊列是沒有容量上限,可是,若是隊列是空的,取元素的操做會阻塞。
  9. DelayQueue包含是Delayed接口的對象:
    interface Delayed extends Comparable< Delayed >
     {
         long getDelay(TimeUnit unit);
     }
    getDelay方法返回對象的殘留延遲。負值表示延遲已經結束。元素只有在延遲用完的狀況下才能從DelayQueue移除。還必須實現compareTo方法。DelayQueue使用該方法對元素進行排序。
  10. Java SE 7增長了一個TransferQueue接口,容許生產者線程等待,直到消費者準備就緒能夠接收一個元素。若是生產者調用
    q.transfer(item);
    這個調用會阻塞,直到另外一個線程將元素(item)刪除。LinkedTransferQueue實現了這個接口。
  11. java.util.concurrent.ArrayBlockingQueue< E > 5.0
    • ArrayBlockingQueue(int capacity)
    • ArrayBlockingQueue(int capacity,boolean fair)
      構造一個帶有指定的容量和公平性的阻塞隊列。該隊列用循環數組實現。
  12. java.util.concurrent.LinkedBlockingQueue< E > 5.0
    java.uti..concurrent.LinkedBlockingDeque< E > 6.0
    • LinkedBlockingQueue()
    • LinkedBlockingDeque()
      構造一個無上限的阻塞隊列或雙向隊列,用鏈表實現。
    • LinkedBolckingQueue(int capacity)
    • LinkedBlockingDeque(int capacity)
      根據指定容量構建一個有限的阻塞隊列或雙向隊列,用鏈表實現。
  13. java.util.concurrent.DelayQueue< E extends Delayed > 5.0
    • DelayQueue()
      構造一個包含Delayed元素的無界的阻塞時間有限的阻塞隊列。只有那些延遲已經超過期間的元素能夠從隊列中移出。
  14. java.util.concurrent.Delayed 5.0
    • long getDelay(TimeUnit unit)
      獲得該對象的延遲,用給定的時間單位進行度量。
  15. java.util.concurrent.PriorityBlockingQueue< E > 5.0
    • PriorityBlockingQueue()
    • PriorityBlockingQueue(int initialCapacity)
    • PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator)
      構造一個無邊界阻塞優先隊列,用堆實現。
      參數:initialCapacity 優先隊列的初始容量。默認值是11。
      comparator 用來對元素進行比較的比較器,若是沒有指定,則元素必須實現Comparable接口。
  16. 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。
  17. 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。
  18. java.util.concurrent.TransferQueue< E > 7
    • void transfer(E element)
    • boolean tryTransfer(E element,long time,TimeUnit unit)
      傳輸一個值,或者嘗試在給定的超時時間內傳輸這個值,這個調用將阻塞,直到另外一個線程將元素刪除。第二個方法會在調用成功時返回true。

14.7 線程安全的集合

  1. 能夠經過提供鎖來保護共享數據結構,可是選擇線程安全的實現做爲替代可能更容易寫。

14.7.1 高效的映射表、集合和隊列

  1. java.util.concurrent包提供了映射表、有序集合隊列的高效實現:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。這些集合使用複雜的算法,經過容許併發地訪問數據結構的不一樣部分來使競爭極小化。
  2. 與大多數集合不一樣,size方法沒必要在常量時間內操做。肯定這樣的集合當前的大小一般須要遍歷。
  3. 集合返回弱一致性(weakly consisteut)的迭代器。這意味着迭代器不必定能反映出它們被構造以後的全部的修改,可是,它們不會將同一個值返回兩次,也不會拋出Concurrent ModificationException異常。
  4. 與之造成對照的是,集合若是在迭代器構造以後發生改變,java.util包中的迭代器將拋出一個ConcurrentModificationException異常。
  5. 併發地散列映射表,可高效地支持大量的讀者和必定數量的寫者。默認狀況下,假定能夠有多達16個寫者線程同時執行。能夠有更多的寫者線程,可是,若是同一時間多於16個,其餘線程將暫時被阻塞。能夠指定更大數目的構造器,然而,沒有這種必要。
  6. ConcurrentHashMap和ConcurrentSkipListMap類有相應的方法用於原子性的關聯插入以及關聯刪除。putIfAbsent方法自動地添加新的關聯,前提是原來沒有這一關聯。對於多線程訪問的緩存來講這是頗有用的,確保只有一個線程向緩存添加項:
    cache.putIfAbsent(key,value);
    相反的操做是刪除(或許應該叫作removeIfPresent)。調用
    cache.remove(key,value);
    將原子性地刪除鍵值對,若是它們在映像表中出現的話。最後,
    cache.replace(key,oldValue,newValue);
    原子性地用新值替換舊值,假定舊值與指定的鍵值關聯。
  7. java.util.concurrent.ConcurrentLinkedQueue< E > 5.0
    • ConcurrentLinkedQueue< E >()
      構造一個能夠被多線程安全訪問的無邊界非阻塞的隊列。
  8. java.util.concurrent.ConcurrentLinkedQueue< E > 5.0
    • ConcurrentSkipListSet< E >()
    • ConcurrentSkipListSet< E >(Comparator<? super E> comp)
      構造一個能夠被多線程安全訪問的有序集。第一個構造器要求元素實現Comparable接口。
  9. 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 寫數組的拷貝

  1. CopyOnWriteArrayList和CopyOnWriteArraySet是線程安全的集合,其中全部的修改線程對底層數組進行復制。若是在集合上進行迭代的線程數超過修改線程數,這樣的安排是頗有用的。當構建一個迭代器的時候,它包含一個對當前數組的引用。若是數組後來被修改了,迭代器仍然引用舊數組,可是,集合的數組已經被替換了。所以,舊的迭代器擁有一致的(可能過期的)視圖,訪問它無須同步開銷。

14.7.3 較早的線程安全集合

  1. 從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 >());
  2. 結果集合的方法使用鎖加以保護,提供了線程的安全訪問。
  3. 應該確保沒有任何線程經過原始的非同步方法訪問數據結構。最便利的方法是確保不保存任何指向原始對象的引用,簡單地構造一個集合並當即傳遞給包裝器。
  4. 若是在另外一個線程可能進行修改時要對集合進行迭代,仍然須要使用「客戶端」鎖定:
    synchronized(synchHashMap)
     {
         Iterator< K > iter = synchHashMap.keySet().iterator();
         while(iter.hashNext())...;
     }
  5. 若是使用「for each」循環必須使用一樣的代碼,由於循環使用了迭代器。注意:若是在迭代過程當中,別的線程修改集合,迭代器會失效,拋出ConcurrentModificationException異常。同步仍然是須要的,所以併發的修改能夠被可靠地檢測出來。
  6. 最好使用java.util.concurrent包中的集合,不使用同步包裝器中的。特別是,加入它們訪問的是不一樣的桶,因爲ConcurrentHashMap已經精心地實現了,多線程能夠訪問它並且不會彼此阻塞。有一個例外是常常被修改的數組列表。在那種狀況下,同步的ArrayList能夠賽過CopyOnWriteArrayList。
  7. 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

  1. Runnable封裝一個異步運行的任務,能夠把它想一想成爲一個沒有參數和返回值的異步方法。Callable與Runnable相似,可是有返回值。Callable接口是一個參數化的類型,只有一個方法call。
    public interface Callable< V >
     {
         V call() throws Exception;
     }
    類型參數是返回值的類型。例如,Callable< Integer >表示一個最終返回Integer對象的異步計算。
  2. Future保存異步計算的結果。能夠啓動一個計算,將Future對象交給某個線程,而後忘掉它。Future對象的全部者在結果計算好以後就能夠得到它。
  3. Future接口具備下面的方法:
    public interface Future< V >
     {
         V get() thros ...;
         V get(long timeout,TimeUnit unit) throwa...;
         void cancel(boolean mayInterupt);
         boolean isCancelled();
            boolean isDone();
    }
    第一個get方法的調用被阻塞,直到計算完成。若是在計算完成以前,第二個方法的調用超時,拋出一個TimeoutException異常。若是運行該計算的線程被中斷,兩個方法都將拋出InterruptedException。若是計算已經完成,那麼get方法當即返回。
    若是計算還在進行,isDone方法返回false;若是完成了,則返回true。
    能夠用cancel方法取消該計算。若是計算尚未開始,它被取消且再也不開始。若是計算處於運行之中,那麼若是mayInterrupt參數爲true,它就被中斷。
  4. FutureTask包裝器是一種很是便利的機制,可將Callable轉換成Future和Runnable,它同時實現兩者的接口。
  5. java.util.concurrent.Callable< V > 5.0
    • V call()
      運行一個將產生結果的任務。
  6. 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。
  7. java.util.concurrent.FutureTask< V > 5.0
    • FutureTask(Callable< V > task)
    • FutureTask(Runnable task,V result)
      構造一個既是Future< V >又是Runnable的對象。

14.9 執行器

  1. 構造一個新的線程是由必定代價的,由於涉及與操做系統的交互。若是程序中建立了大量的生命週期很短的線程,應該使用線程池(thread pool)。一個線程池中包含許多準備運行的空閒線程。將Runnable對象交給線程池,就會有一個線程調用run方法。當run方法退出時,線程不會死亡,而是在池中準備爲下一個請求提供服務。
  2. 另外一個使用線程池的理由是減小併發線程的數目。建立大量線程會大大下降性能甚至使虛擬機崩潰。若是有一個會建立許多線程的算法,應該使用線程數「固定的」線程池以限制併發線程的總數。
  3. 執行器(Executor)類有許多靜態工廠方法用來構建線程池。
  4. 執行者工廠方法
    方法 描述
    newCachedThreadPool 必要時建立新線程;空閒線程會被保留60秒
    newFixedThreadPool 該池包含固定數量的線程;空閒線程會一直被保留
    newSingleThreadExecutor 只有一個線程的「池」,該線程順序執行每個提交的任務(相似於Swing事件分配線程)
    newScheduledThreadPool 用於預約執行而構建的固定線程池,替代java.util.Timer
    newSingleThreadScheduleExecutor 用於預約執行而構建的單線程「池」

14.9.1 線程池

  1. newCachedThreadPool方法構建了一個線程池。對於每一個任務,若是有空閒線程可用,當即讓它執行任務,若是沒有可用的空閒線程,則構建一個新線程。newFixedThreadPool方法構建一個具備固定大小的線程池。若是提交的任務數多於空閒的線程數,那麼把得不到服務的任務放置到隊列中。當其餘任務完成之後再運行它們。newSingleThreadExecutor是一個退化了的大小爲1的線程池:由一個線程執行提交的任務,一個接着一個。這3個方法返回實現了ExecutorService接口的ThreadPoolExecutor類的對象。
  2. 可用下面的方法之一將一個Runnable對象或Callable對象提交給ExecutorService:
    Future<?> submit(Runnable task)
     Future< T > submit(Runnable task,T result)
     Future< T > submit(Callable< T > task)
    該池會在方便的時候儘早執行提交的任務。調用submit時,會獲得一個Future對象,可用來查詢該任務的狀態。
    第一個submit方法返回一個奇怪樣子的Future<?>。可使用這樣一個對象來調用isDone、cancel或isCancelled。可是,get方法在完成的時候只是簡單地返回null。
    第二個版本的Submit也提交一個Runnable,而且Future的get方法在完成的時候返回指定的result對象。
    第三個版本的Submit提交一個Callable,而且返回的Future對象將在計算結果準備好的時候獲得它。
  3. 當用完一個線程池的時候,調用shutdown。該方法啓動該池的關閉序列。被關閉的執行器再也不接受新的任務。當全部任務都完成之後,線程池中的線程死亡。另外一個方法是調用shutdownNow。該池取消還沒有開始的全部任務並試圖中斷正在運行的線程。
  4. 下面總結了在使用鏈接池時應該作的事:
    (1)調用Executors類中靜態的方法newCachedThreadPool或newFixedThreadPool。
    (2)調用submit提交Runnable或Callable對象。
    (3)若是想要取消一個任務,或若是提交Callable對象,那就要保存好返回的Future對象。
    (4)當再也不提交任何任務時,調用shutdown。
  5. java.util.concurrent.Executors 5.0
    • ExecutorService newCachedThreadPool()
      返回一個帶緩存的線程池,該池在必要的時候建立線程,在線程空閒60秒以後終止線程。
    • ExecutorService newFixedThreadPool(int threads)
      返回一個線程池,該池中的線程數由參數指定。
    • ExecutorService newSingleThreadExecutor()
      返回一個執行器,它在一個單個的線程中一次執行各個任務。
  6. 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()
      關閉服務,會先按成已經提交的任務而再也不接收新的任務。
  7. java.util.concurrent.ThreadPoolExecutor 5.0
    • int getLargestPoolSize()
      返回線程池在該執行器盛行週期中的最大尺寸。

14.9.2 預約執行

  1. ScheduleExecutorService接口具備爲預約執行(Scheduled Execution)或重複執行任務而設計的方法。它是一種容許使用線程池機制的java.util.Timer的泛化。Executors類的newScheduledThreadPool和newSingleThreadScheduledExecutor方法將返回實現了ScheduledExecutorService接口的對象。
  2. 能夠預約Runnable或Callable在初始的延遲以後只運行一次。也能夠預約一個Runnable對象週期性地運行。
  3. java.util.concurrent.Executors 5.0
    • ScheduledExcutorService newScheduledThreadPool(int threads)
      返回一個線程池,它使用給定的線程數來調度任務。
    • ScheduledExecutorService newSingleThreadScheduledExecutor()
      返回一個執行器,它在一個單獨線程中調度任務。
  4. 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 控制任務組

  1. invokeAny方法提交全部對象到一個Callable對象的集合中,並返回某個已經完成了的任務的結果。沒法知道返回的到底是哪一個任務的結果,也許是最早完成的那個任務的結果。對於搜素問題,若是你願意接受任何一種解決方案的話,就可使用這個方法。
  2. invokeAll方法提交全部對象到一個Callable對象的集合中,並返回一個Future對象的列表,表明全部任務的解決方案。當計算結果可得到時,能夠像下面這樣對結果進行處理:
    List<Callable< T >> tasks=...;
     List<Future< T >> results =             executor.invokeAll(tasks);
     for (Future< T > result:results)
         processFurther(result.get());
    這個方法的缺點是若是第一個任務恰巧花去了不少時間,則可能按可得到的順序保存起來更有實際意義。能夠用ExecutorCompletionService來進行排列。
  3. 用常規的方法得到一個執行器。而後,構建一個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());
  4. 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超時。
  5. 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。第二個方法將等待給定的時間。

14.9.4 Fork-Join框架

  1. 有些應用使用了大量線程,但其中大多數都是空閒的。舉例來講,一個Web服務器可能會爲每一個鏈接分別使用一個線程。另一些應用可能對每一個處理器內核分別使用一個線程,來完成計算密集型任務,如圖像或視頻處理。Java SE 7中新引入了fork-join框架,專門用來支持後一類應用。假設有一個處理任務,它能夠很天然地分解爲子任務,以下所示:
    if(problemSize > threshold)
         solve problem direckly
     else
     {
         break problem into subproblems
         recursively solve each subproblem
         combine the results
     }
  2. 在後臺,fork-join框架使用了一種有效的智能方法來平衡可用線程的工做負載,這種方法稱爲工做密取(work stealing)。每一個工做線程都有一個雙端隊列(deque)來完成任務。一個工做線程將子任務壓入其雙端隊列的隊頭。(只有一個線程能夠訪問隊頭,因此不須要加鎖。)一個工做線程空閒時,它會從另外一個雙端隊列的隊尾「密取」一個任務。因爲大的子任務都在隊尾,這種密取不多出現。

14.10 同步器

  1. java.util.concurrent包包含了幾個能幫助人們管理相互合做的線程集的類見下表。這些機制具備爲線程之間的共用集結點模式(common rendezous patterns)提供的「預置功能」(canned functionality)。若是有一個相互合做的線程集知足這些行爲模式之一,那麼應該直接重用合適的庫類而不要試圖提供手工的鎖與條件的集合。
    同步器
    它能作什麼 什麼時候使用
    CyclicBarrier 容許線程集等待直至其中預約數目的線程到達一個公共障柵(barrier),而後能夠選擇執行一個處理障柵的動做 當大量的線程須要在它們的結果可用以前完成時
    CountDownLatch 容許線程集等待直到計數器減爲0 當一個或多個線程須要等待直到指定數目的事件發生
    Exchanger 容許兩個線程在要交換的對象準備好時交換對象 當兩個線程工做在同一個數據結構的兩個實例上的時候,一個向實例添加數據而另外一個從實例清除數據
    Semaphore 容許線程集等待直到被容許繼續運行爲止 限制訪問資源的線程總數。若是許可數是1,經常阻塞線程直到另外一個線程給出許可爲止
    SynchronoutQueue 容許一個線程把對象交給另外一個線程 在沒有顯式同步的狀況下,當兩個線程準備好將一個對象從一個線程傳遞到另外一個時

14.10.1 信號量

  1. 概念上講,一個信號量管理許多的許可證( permits )。爲了經過信號量,線程經過調用 acquire 請求許可。其實沒有實際的許可對象,信號量僅維護一個計數。許可的數目是固定的,由此限制了經過的線程數量。其餘線程能夠經過調用 release 釋放許可。並且,許可不是必須由獲取它的線程釋放。事實上,任何線程均可以釋聽任意數目的許可,這可能會增長許可數目以致於超出初始數目。
  2. 信號量在 1968 年由 Edsger Dijkstra 發明,做爲同步原語( synchronization primitive )。 Dijkstra指出信號量能夠被有效地實現,而且有足夠的能力解決許多常見的線程同步問題。在幾乎任何一本操做系統教科書中,都能看到使用信號量實現的有界隊列。
    應用程序員沒必要本身實現有界隊列。一般,信號量沒必要直接映射到通用應用場景。

14.10.2 倒計時門栓

  1. 一個倒計時門栓( CountDownLatch )讓一個線程集等待直到計數變爲 0 。倒計時門栓是一次性的。一旦計數爲 0 ,就不能再重用了。
  2. 一個有用的特例是計數值爲 1 的門栓。實現一個只能經過一次的門。線程在門外等候直到另外一個線程將計數器值置爲 0 。

14.10.3 障柵

  1. CyclicBarrier 類實現了一個集結點( rendezvous )稱爲障柵( barrier )。考慮大量線程運行在一次計算的不一樣部分的情形。當全部部分都準備好時,須要把結果組合在一塊兒。當一個線程完成了它的那部分任務後,咱們讓它運行到障柵處。一旦全部的線程都到達了這個障柵,障柵就撤銷,線程就能夠繼續運行。
  2. 若是任何一個在障柵上等待的線程離開了障柵,那麼障柵就被破壞了(線程可能離開是由於它調用 await 時設置了超時,或者由於它被中斷了)。在這種狀況下,全部其餘線程的 await 方法拋出 BrokenBarrierException 異常。那些已經在等待的線程當即終止 await 的調用。
  3. 障柵被稱爲是循環的( cyclic ),由於能夠在全部等待線程被釋放後被重用。在這一點上,有別於 CountDownLatch , CountDownLatch 只能被運行一次。
  4. Phaser 類增長了更大的靈活性,云溪改變不一樣階段中參與線程的個數。

14.10.4 交換器

  1. 當兩個線程在同一個數據緩衝區的兩個實例上工做的時候,就可使用變換器( Exchanger )。典型的狀況是,一個線程想緩衝區填入數據,另外一個線程消耗這些數據。當它們都完成之後,相互交換緩衝區。

14.10.5 同步隊列

  1. 同步隊列是一種將生產者與消費者線程配對的機制。當一個線程調用 SynchronousQueue 的 put 方法時,它會阻塞直到另外一個線程調用 take 方法爲止,反之亦然。與 Exchanger 的狀況不一樣,數據僅僅沿一個方向傳遞,從生產者到消費者。
  2. 即便 SynchronousQueue 類實現了 BlockingQueue 接口,概念上講,它依然不是一個隊列。它沒有包含任何元素,它的 size 方法老是返回 0 。

14.11 線程與Swing

  1. 在程序中使用線程的理由之一是提升程序的響應性能。當程序須要作某些耗時的工做時,應該啓動另外一個工做器線程而不是阻塞用戶接口。
  2. 必須認真考慮工做器線程在作什麼,由於這或許使人驚訝, Swing 不是線程安全的。若是試圖在多個縣城中操縱用戶界面的元素,那麼用戶界面可能奔潰。

14.11.1 運行耗時的任務

  1. 將線程與 Swing 一塊兒使用時,必須遵循兩個簡單的原則。java

    • 若是一個動做須要花費很長時間,在一個獨立的工做器線程中作這件事不要在事件分配線程中作。
    • 除了事件分配線程,不要在任何線程中接觸 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 方法。沒有新的線程被建立。程序員

  2. 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。

14.11.2 使用Swing工做線程

  1. SwingWorker 類覆蓋 doInBackground 方法來完成耗時的工做,調用 publish 來報告工做進度。這一方法在工做器線程中執行。 publish 方法使得 process 方法在事件分配線程中執行來處理進度數據。當工做完成時, done 方法在事件分配線程中被調用以便完成 UI 的更新。
      每當要在工做器線程中作一些工做時,構建一個新的工做器(每個工做器對象只能被使用一次)。而後調用 execute 方法。典型的方式是在事件分配線程中調用 execute ,但沒有這樣的需求。
  2. SwingWorker 類有 3 種類型做爲類型參數。 SwingWorker< T,V > 產生類型爲 T 的結果以及類型爲 V 的進度數據。
  3. 要取消正在進行的工做,使用 Future 接口的 cancel 方法。當該工做被取消的時候, get 方法拋出 CancellationException 異常。
  4. 工做器線程對 publish 的調用會致使在事件分配線程上的 process 的調用。爲了提升效率,幾個對 publish 的調用結果,可用對 process 的一次調用成批處理。 process 方法接收一個包含全部中間結果的列表< V > 。
  5. 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 之一。

14.11.3 單一線程規則

  1. 單一線程規則:「除了事件分配線程,不要在任何線程中接觸 Swing 組件」。
  2. 對於單一線程規則存在一些例外狀況:
    • 可在任一個線程裏添加或移除事件監聽器。固然該監聽器的方法會在事件分配線程中被觸發。
    • 只有不多的 Swing 方法是線程安全的。在 API 文檔中用這樣的句子特別標明: 「儘管大多數 Swing 方法不是線程安全的,但這個方法是。」在這些線程安全的方法中最有用的是:
      JTextComponent.setText
      JtextArea.insert
      JTextArea.append
      JTextArea.replaceRange
      JComponent.repaint
      JComponent.revalidate
  3. 歷史上,單一線程規則是更加隨意的。任何線程均可以構建組件,設置優先級,將它們添加到容器中,只要這些組件沒有一個是已經被實現的( realized )。若是組件能夠接受 paint 事件或 validation 事件,組件被實現。一旦調用組件的 setVisible(true) 或 pack(!) 方法或者組件已經被添加到已經被實現的容器中,就出現這樣的狀況。
相關文章
相關標籤/搜索