Java併發教程(Oracle官方資料)

本文是Oracle官方的Java併發相關的教程,感謝併發編程網的翻譯和投遞。

(關注ITeye官微,隨時隨地查看最新開發資訊、技術文章。)

計算機的使用者一直覺得他們的計算機能夠同時作不少事情。他們認爲當其餘的應用程序在下載文件,管理打印隊列或者緩衝音頻的時候他們能夠繼續在文 字處理程序上工做。甚至對於單個應用程序,他們任然期待它能在在同一時間作不少事情。舉個例子,一個流媒體播放程序必須能同時完成如下工做:從網絡上讀取 數字音頻,解壓縮數字音頻,管理播放和更新程序顯示。甚至文字處理器也應該能在忙於從新格式化文本和刷新顯示的狀況下同時響應鍵盤和鼠標事件。這樣的軟件 就被稱爲併發軟件。

經過Java語言和Java類庫對於基礎併發的支持,Java平臺具備徹底(from the ground up )支持併發編程的能力。從JDK5.0起,Java平臺還引入了高級併發APIs。這個課程不只涵蓋了Java平臺基礎併發內容,還對高級併發APIs有 必定的闡述。html

目 錄 [ - ]

  1. 進程和線程java

  2. 線程對象c++

  3. 同步程序員

  4. 活躍度web

  5. 保護塊(Guarded Blocks)算法

  6. 不可變對象express

  7. 高級併發對象編程

進程和線程        Top              



(本部分原文連接譯文連接,譯者:bjsuo,校對:鄭旭東)
在併發編程中,有兩個基本的執行單元:進程和線程。在java語言中,併發編程最關心的是線程,然而,進程也是很是重要的。

即便在只有單一的執行核心的計算機系統中,也有許多活動的進程和線程。所以,在任何給定的時刻,只有一個線程在實際執行。處理器的處理時間是經過操做系統的時間片在進程和線程中共享的。

如今具備多處理器或有多個執行內核的多處理器的計算機系統愈來愈廣泛,這大大加強了系統併發執行的進程和線程的吞吐量–但在不沒有多個處理器或執行內核的簡單的系統中,併發任然是可能的。

進程

進程具備一個獨立的執行環境。一般狀況下,進程擁有一個完整的、私有的基本運行資源集合。特別地,每一個進程都有本身的內存空間。

進程每每被看做是程序或應用的代名詞,然而,用戶看到的一個單獨的應用程序實際上多是一組相互協做的進程集合。爲了便於進程之間的通訊,大多數操做系統都支持進程間通訊(IPC),如pipes 和sockets。IPC不只支持同一系統上的通訊,也支持不一樣的系統。

Java虛擬機的大多數實現是單進程的。Java應用可使用的ProcessBuilder對象建立額外的進程,多進程應用超出了本課的範圍。

線程

線程有時也被稱爲輕量級的進程。進程和線程都提供了一個執行環境,但建立一個新的線程比建立一個新的進程須要的資源要少。

線程是在進程中存在的 — 每一個進程最少有一個線程。線程共享進程的資源,包括內存和打開的文件。這樣提升了效率,但潛在的問題就是線程間的通訊。

多線程的執行是Java平臺的一個基本特徵。每一個應用都至少有一個線程 – 或幾個,若是算上「系統」線程的話,好比內存管理和信號處理等。可是從程序員的角度來看,啓動的只有一個線程,叫主線程。這個線程有能力建立額外的線程,咱們將在下一節演示。
     api

線程對象        Top              


(本部分原文連接譯文連接,譯者:鄭旭東)
在Java中,每一個線程都是Thread類的實例。併發應用中通常有兩種不一樣的線程建立策略。

數組

  • 直接控制線程的建立和管理,每當應用程序須要執行一個異步任務的時候就爲其建立一個線程

  • 將線程的管理從應用程序中抽象出來做爲執行器,應用程序將任務傳遞給執行器,有執行器負責執行。

這一節,咱們將討論Thread對象,有關Executors將在高級併發對象一節中討論。

定義並啓動一個線程

應用程序在建立一個線程實例時,必須提供須要在線程中運行的代碼。有兩種方式去作到這一點:

  • 提供一個Runnable對象。Runnable對象僅包含一個run()方法,在這個方法中定義的代碼將在會線程中執行。將Runnable對象傳遞給Thread類的構造函數便可,以下面這個HelloRunnable的例子:


Java代碼

  1. public class HelloRunnable implements Runnable {  

  2.   

  3.     public void run() {  

  4.         System.out.println("Hello from a thread!");  

  5.     }  

  6.   

  7.     public static void main(String args[]) {  

  8.         (new Thread(new HelloRunnable())).start();  

  9.     }  

  10.   

  11. }  




  • 繼承Thread類。Thread類自身已實現了Runnable接口,但它的run()方法中並無定義任何代碼。應用程序能夠繼承與Thread類,並複寫run()方法。如例子HelloThread


Java代碼

  1. public class HelloThread extends Thread {  

  2.   

  3.     public void run() {  

  4.         System.out.println("Hello from a thread!");  

  5.     }  

  6.   

  7.     public static void main(String args[]) {  

  8.         (new HelloThread()).start();  

  9.     }  

  10.   

  11. }  



須要注意的是,上述兩個例子都須要調用Thread.start()方法來啓動一個新的線程。 哪種方式是咱們應該使用的?相對來講,第一種更加通用,由於Runnable對象能夠繼承於其餘類(Java只支持單繼承,當一個類繼承與Thread 類後,就沒法繼承與其餘類)。第二種方法更易於在簡單的應用程序中使用,但它的侷限就是:你的任務類必須是Thread的子類。這個課程更加聚焦於第一種 將Runnable任務和Thread類分離的方式。不只僅是由於這種方式更加靈活,更由於它更適合後面將要介紹的高級線程管理API。 Thread類定義了一些對線程管理十分有用的的方法。在這些方法中,有一些靜態方法能夠給當前線程調用,它們能夠提供一些有關線程的信息,或者影響線程 的狀態。而其餘一些方法能夠由其餘線程進行調用,用於管理線程和Thread對象。咱們將在下面的章節中,深刻探討這些內容。

使用Sleep方法暫停一個線程

使用Thread.sleep()方法能夠暫停當前線程一段時間。這是一種使處理器時間能夠被其餘線程或者運用程序使用的有效方式。sleep()方法還能夠用於調整線程執行節奏(見下面的例子)和等待其餘有執行時間需求的線程(這個例子將在下一節演示)。
在Thread中有兩個不一樣的sleep()方法,一個使用毫秒錶示休眠的時間,而另外一個是用納秒。因爲操做系統的限制休眠時間並不能保證十分精 確。休眠週期能夠被interrups所終止,咱們將在後面看到這樣的例子。無論在任何狀況下,咱們都不該該假定調用了sleep()方法就能夠將一個線 程暫停一個十分精確的時間週期。

SleepMessages程序爲咱們展現了使用sleep()方法每四秒打印一個信息的例子

Java代碼

  1. public class SleepMessages {  

  2.     public static void main(String args[])  

  3.         throws InterruptedException {  

  4.         String importantInfo[] = {  

  5.             "Mares eat oats",  

  6.             "Does eat oats",  

  7.             "Little lambs eat ivy",  

  8.             "A kid will eat ivy too"  

  9.         };  

  10.   

  11.         for (int i = 0;  

  12.              i < importantInfo.length;  

  13.              i++) {  

  14.             //Pause for 4 seconds  

  15.             Thread.sleep(4000);  

  16.             //Print a message  

  17.             System.out.println(importantInfo[i]);  

  18.         }  

  19.     }  

  20. }  




main()方法聲明瞭它有可能拋出InterruptedException。當其餘線程中斷當前線程時,sleep()方法就會拋出該異常。因爲這個應用程序並無定義其餘的線程,因此並不用關心如何處理該異常。

中斷(Interrupts)

中斷是給線程的一個指示,告訴它應該中止正在作的事並去作其餘事情。一個線程究竟要怎麼響應中斷請求取決於程序員,不過讓其終止是很廣泛的作法。這是本文重點強調的用法。

一個線程經過調用對被中斷線程的Thread對象的interrupt()方法,發送中斷信號。爲了讓中斷機制正常工做,被中斷的線程必須支持它本身的中斷(即要本身處理中斷)

中斷支持

線程如何支持自身的中斷?這取決於它當前正在作什麼。若是線程正在頻繁調用會拋InterruptedException異常的方法,在捕獲異常 以後,它只是從run()方法中返回。例如,假設在SleepMessages的例子中,關鍵的消息循環在線程的Runnable對象的run方法中,代 碼可能會被修改爲下面這樣以支持中斷:

Java代碼

  1. for (int i = 0; i < importantInfo.length; i++) {  

  2.     // Pause for 4 seconds  

  3.     try {  

  4.        Thread.sleep(4000);  

  5.     } catch (InterruptedException e) {  

  6.        // We've been interrupted: no more messages.  

  7.       return;  

  8.  }  

  9.  // Print a message  

  10.  System.out.println(importantInfo[i]);  

  11. }  




許多會拋InterruptedException異常的方法(如sleep()),被設計成接收到中斷後取消它們當前的操做,並在當即返回。

若是一個線程長時間運行而不調用會拋InterruptedException異常的方法會怎樣? 那它必須週期性地調用Thread.interrupted()方法,該方法在接收到中斷請求後返回true。例如:

Java代碼

  1. for (int i = 0; i < inputs.length; i++) {  

  2.     heavyCrunch(inputs[i]);  

  3.     if (Thread.interrupted()) {  

  4.         // We've been interrupted: no more crunching.  

  5.         return;  

  6.     }  

  7. }  



在這個簡單的例子中,代碼只是檢測中斷,並在收到中斷後退出線程。在更復雜的應用中,拋出一個InterruptedException異常可能更有意義。

Java代碼

  1. if (Thread.interrupted()){  

  2.    throw new InterruptedException();  

  3. }  



這使得中斷處理代碼能集中在catch語句中。

中斷狀態標記

中斷機制經過使用稱爲中斷狀態的內部標記來實現。調用Thread.interrupt()設置這個標記。當線程經過調用靜態方法 Thread.interrupted()檢測中斷時,中斷狀態會被清除。非靜態的isInterrupted()方法被線程用來檢測其餘線程的中斷狀 態,不改變中斷狀態標記。

按照慣例,任何經過拋出一個InterruptedException異常退出的方法,當拋該異常時會清除中斷狀態。不過,經過其餘的線程調用interrupt()方法,中斷狀態老是有可能會當即被從新設置。

Joins

Join()方法可讓一個線程等待另外一個線程執行完成。若t是一個正在執行的Thread對象,

Java代碼

  1. t.join();  



將會使當前線程暫停執行並等待t執行完成。重載的join()方法可讓開發者自定義等待週期。然而,和sleep()方法同樣join()方法依賴於操做系統的時間處理機制,你不能假定join()方法將會精確的等待你所定義的時長。
如同sleep()方法,join()方法響應中斷並在中斷時拋出InterruptedException。

一個簡單的線程例子

下面這個簡單的例子將會把這一節的一些概念放到一塊兒演示。SimpleThreads程序有兩個線程組成,第一個是主線程,它從建立了一個線程並等待它執行完成。若是MessageLoop線程執行了太長時間,主線程將會將其中斷。
MessageLoop現場將會打印一系列的信息。若是中斷在它打印完全部信息前發生,它將會打印一個特定的消息並退出。

Java代碼

  1. public class SimpleThreads {  

  2.   

  3.     // Display a message, preceded by  

  4.     // the name of the current thread  

  5.     static void threadMessage(String message) {  

  6.         String threadName =  

  7.             Thread.currentThread().getName();  

  8.         System.out.format("%s: %s%n",  

  9.                           threadName,  

  10.                           message);  

  11.     }  

  12.   

  13.     private static class MessageLoop  

  14.         implements Runnable {  

  15.         public void run() {  

  16.             String importantInfo[] = {  

  17.                 "Mares eat oats",  

  18.                 "Does eat oats",  

  19.                 "Little lambs eat ivy",  

  20.                 "A kid will eat ivy too"  

  21.             };  

  22.             try {  

  23.                 for (int i = 0;  

  24.                      i < importantInfo.length;  

  25.                      i++) {  

  26.                     // Pause for 4 seconds  

  27.                     Thread.sleep(4000);  

  28.                     // Print a message  

  29.                     threadMessage(importantInfo[i]);  

  30.                 }  

  31.             } catch (InterruptedException e) {  

  32.                 threadMessage("I wasn't done!");  

  33.             }  

  34.         }  

  35.     }  

  36.   

  37.     public static void main(String args[])  

  38.         throws InterruptedException {  

  39.   

  40.         // Delay, in milliseconds before  

  41.         // we interrupt MessageLoop  

  42.         // thread (default one hour).  

  43.         long patience = 1000 * 60 * 60;  

  44.   

  45.         // If command line argument  

  46.         // present, gives patience  

  47.         // in seconds.  

  48.         if (args.length > 0) {  

  49.             try {  

  50.                 patience = Long.parseLong(args[0]) * 1000;  

  51.             } catch (NumberFormatException e) {  

  52.                 System.err.println("Argument must be an integer.");  

  53.                 System.exit(1);  

  54.             }  

  55.         }  

  56.   

  57.         threadMessage("Starting MessageLoop thread");  

  58.         long startTime = System.currentTimeMillis();  

  59.         Thread t = new Thread(new MessageLoop());  

  60.         t.start();  

  61.   

  62.         threadMessage("Waiting for MessageLoop thread to finish");  

  63.         // loop until MessageLoop  

  64.         // thread exits  

  65.         while (t.isAlive()) {  

  66.             threadMessage("Still waiting...");  

  67.             // Wait maximum of 1 second  

  68.             // for MessageLoop thread  

  69.             // to finish.  

  70.             t.join(1000);  

  71.             if (((System.currentTimeMillis() - startTime) > patience)  

  72.                   && t.isAlive()) {  

  73.                 threadMessage("Tired of waiting!");  

  74.                 t.interrupt();  

  75.                 // Shouldn't be long now  

  76.                 // -- wait indefinitely  

  77.                 t.join();  

  78.             }  

  79.         }  

  80.         threadMessage("Finally!");  

  81.     }  

  82. }  



     

同步        Top              



(本部分原文連接譯文連接,譯者:蘑菇街-小寶,Greenster李任  校對:丁一,鄭旭東,李任
線程間的通訊主要是經過共享域和引用相同的對象。這種通訊方式很是高效,不過可能會引起兩種錯誤:線程干擾和內存一致性錯誤。防止這些錯誤發生的方法是同步。

不過,同步會引發線程競爭,當兩個或多個線程試圖同時訪問相同的資源,隨之就致使Java運行時環境執行其中一個或多個線程比原先慢不少,甚至執行被掛起,這就出現了線程競爭。線程飢餓和活鎖都屬於線程競爭的範疇。關於線程競爭的更多信息可參考活躍度一節。

本節內容包括如下這些主題:

  • 線程干擾討論了當多個線程訪問共享數據時錯誤是怎麼發生的。

  • 內存一致性錯誤討論了不一致的共享內存視圖致使的錯誤。

  • 同步方法討論了 一種能有效防止線程干擾和內存一致性錯誤的常見作法。

  • 內部鎖和同步討論了更通用的同步方法,以及同步是如何基於內部鎖實現的。

  • 原子訪問討論了不能被其餘線程干擾的操做的整體思路。

1.  線程干擾

下面這個簡單的Counter類:

Java代碼

  1. class Counter {  

  2.     private int c = 0;  

  3.     public void increment() {  

  4.         c++;  

  5.     }  

  6.     public void decrement() {  

  7.         c--;  

  8.     }  

  9.     public int value() {  

  10.         return c;  

  11.     }  

  12. }  



Counter類被設計成:每次調用increment()方法,c的值加1;每次調用decrement()方法,c的值減1。若是當同一個Counter對象被多個線程引用,線程間的干擾可能會使結果同咱們預期的不一致。

當兩個運行在不一樣的線程中卻做用在相同的數據上的操做交替執行時,就發生了線程干擾。這意味着這兩個操做都由多個步驟組成,而步驟間的順序產生了重疊。

Counter類實例的操做會交替執行,這看起來彷佛不太可能,由於c上的這兩個操做都是單一而簡單的語句。然而,即便一個簡單的語句也會被虛擬機轉換成多個步驟。咱們不去深究虛擬機內部的詳細執行步驟——理解c++這個單一的語句會被分解成3個步驟就足夠了:

  • 獲取當前c的值;

  • 對獲取到的值加1;

  • 把遞增後的值寫回到c;

語句c–也能夠按一樣的方式分解,除了第二步的操做是遞減而不是遞增。

假設線程A調用increment()的同時線程B調用decrement().若是c的初始值爲0,線程A和B之間的交替執行順序多是下面這樣:

  • 線程A:獲取c;

  • 線程B:獲取c;

  • 線程A:對獲取的值加1,結果爲1;

  • 線程B:對獲取的值減1,結果爲-1;

  • 線程A:結果寫回到c,c如今是1;

  • 線程B:結果寫回到c,c如今是-1;

線程A的結果由於被線程B覆蓋而丟失了。這個交替執行的結果只是其中一種可能性。在不一樣的環境下,多是線程B的結果丟失了,也多是不會出任何問題。因爲結果是不可預知的,因此線程干擾的bug很難檢測和修復。

2.  內存一致性錯誤

當不一樣的線程對相同的數據產生不一致的視圖時會發生內存一致性錯誤。內存一致性錯誤的緣由比較複雜,也超出了本教程的範圍。不過幸運的是,一個程序員並不須要對這些緣由有詳細的瞭解。所須要的是避免它們的策略。

避免內存一致性錯誤的關鍵是理解happens-before關係。這種關係只是確保一個特定語句的寫內存操做對另一個特定的語句可見。要說明這個問題,請參考下面的例子。假設定義和初始化了一個簡單int字段:

Java代碼

  1. int counter =0 ;  



這個counter字段被A,B兩個線程共享。假設線程A對counter執行遞增:

Java代碼

  1. counter++;  



而後,很快的,線程B輸出counter:

Java代碼

  1. System.out.println(counter);  



若是這兩個語句已經在同一個線程中被執行過,那麼輸出的值應該是「1」。不過若是這兩個語句在不一樣的線程中分開執行,那輸出的值極可能是「0」, 由於沒法保證線程A對counter的改動對線程B是可見的——除非咱們在這兩個語句之間已經創建了happens-before關係。

有許多操做會創建happens-before關係。其中一個是同步,咱們將在下面的章節中看到。
咱們已經見過兩個創建happens-before關係的操做。

當一條語句調用Thread.start方法時,和該語句有happens-before關係的每一條語句,跟新線程執行的每一條語句一樣有happens-before關係。建立新線程以前的代碼的執行結果對線新線程是可見的。

當一個線程終止而且當致使另外一個線程中Thread.join返回時,被終止的線程執行的全部語句和在join返回成功以後的全部語句間有happens-before關係。線程中代碼的執行結果對執行join操做的線程是可見的。

要查看創建happens-before關係的操做列表,請參閱java.util.concurrent包的摘要頁面

3.  同步方法

Java編程語言提供兩種同步方式:同步方法和同步語句。相對較複雜的同步語句將在下一節中介紹。本節主要關注同步方法。

要讓一個方法成爲同步方法,只須要在方法聲明中加上synchronized關鍵字:

Java代碼

  1. public class SynchronizedCounter {  

  2.     private int c = 0;  

  3.   

  4.     public synchronized void increment() {  

  5.         c++;  

  6.     }  

  7.   

  8.     public synchronized void decrement() {  

  9.         c--;  

  10.     }  

  11.   

  12.     public synchronized int value() {  

  13.         return c;  

  14.     }  

  15. }  



若是count是SynchronizedCounter類的實例,那麼讓這些方法成爲同步方法有兩個做用:
首先,相同對象上的同步方法的兩次調用,它們要交替執行是不可能的。 當一個線程正在執行對象的同步方法時,全部其餘調用該對象同步方法的線程會被阻塞(掛起執行),直到第一個線程處理完該對象。

其次,當一個同步方法退出時,它會自動跟該對象同步方法的任意後續調用創建起一種happens-before關係。這確保對象狀態的改變對全部線程是可見的。

注意構造方法不能是同步的——構造方法加synchronized關鍵字會報語法錯誤。同步的構造方法沒有意義,由於當這個對象被建立的時候,只有建立對象的線程能訪問它。

警告:當建立的對象會被多個線程共享時必須很是當心,對象的引用不要過早「暴露」出去。好比,假設你要維護一個叫instances的List,它包含類的每個實例對象。你可能會嘗試在構造方法中加這樣一行:

Java代碼

  1. instances.add(this);  



不過其餘線程就可以在對象構造完成以前使用instances訪問對象。

同步(synchronized)方法使用一種簡單的策略來防止線程干擾和內存一致性錯誤:若是一個對象對多個線程可見,對象域上的全部讀寫操做 都是經過synchronized方法來完成的。(一個重要的例外:final域,在對象被建立後不可修改,能被非synchronized方法安全的讀 取)。synchronized同步策略頗有效,不過會引發活躍度問題,咱們將在本節後面看到。

4.  內部鎖與同步

同步機制的創建是基於其內部一個叫內部鎖或者監視鎖的實體。(在Java API規範中一般被稱爲監視器。)內部鎖在同步機制中起到兩方面的做用:對一個對象的排他性訪問;創建一種happens-before關係,而這種關係正是可見性問題的關鍵所在。

每一個對象都有一個與之關聯的內部鎖。一般當一個線程須要排他性的訪問一個對象的域時,首先須要請求該對象的內部鎖,當訪問結束時釋放內部鎖。在線 程得到內部鎖到釋放內部鎖的這段時間裏,咱們說線程擁有這個內部鎖。那麼當一個線程擁有一個內部鎖時,其餘線程將沒法得到該內部鎖。其餘線程若是去嘗試獲 得該內部鎖,則會被阻塞。

當線程釋放一個內部鎖時,該操做和對該鎖的後續請求間將創建happens-before關係。

5.  同步方法中的鎖

當線程調用一個同步方法時,它會自動請求該方法所在對象的內部鎖。當方法返回結束時則自動釋放該內部鎖,即便退出是因爲發生了未捕獲的異常,內部鎖也會被釋放。

你可能會問調用一個靜態的同步方法會如何,因爲靜態方法是和類(而不是對象)相關的,因此線程會請求類對象(Class Object)的內部鎖。所以用來控制類的靜態域訪問的鎖不一樣於控制對象訪問的鎖。

6.  同步塊

另一種同步的方法是使用同步塊。和同步方法不一樣,同步塊必須指定所請求的是哪一個對象的內部鎖:

Java代碼

  1. public void addName(String name) {  

  2.     synchronized(this) {  

  3.         lastName = name;  

  4.         nameCount++;  

  5.     }  

  6.     nameList.add(name);  

  7. }  



在上面的例子中,addName方法須要使lastName和nameCount的更改保持同步,並且要避免同步調用該對象的其餘方法。(在同步代碼中調用其餘方法會產生Liveness一節所描述的問題。)若是不使用同步塊,那麼必需要定義一個額外的非同步方法,而這個方法僅僅是用來調用nameList.add。

使用同步塊對於更細粒度的同步頗有幫助。例如類MsLunch有兩個實例域c1和c2,他們並不會同時使用(譯者注:即c1和c2是彼此無關的兩 個域),全部對這兩個域的更新都須要同步,可是徹底不須要防止c1的修改和c2的修改相互之間干擾(這樣作只會產生沒必要要的阻塞而下降了併發性)。這種情 況下沒必要使用同步方法,可使用和this對象相關的鎖。這裏咱們建立了兩個「鎖」對象(譯者注:起到加鎖效果的普通對象lock1和lock2)。

Java代碼

  1. public class MsLunch {  

  2.     private long c1 = 0;  

  3.     private long c2 = 0;  

  4.     private Object lock1 = new Object();  

  5.     private Object lock2 = new Object();  

  6.   

  7.     public void inc1() {  

  8.         synchronized(lock1) {  

  9.             c1++;  

  10.         }  

  11.     }  

  12.   

  13.     public void inc2() {  

  14.         synchronized(lock2) {  

  15.             c2++;  

  16.         }  

  17.     }  

  18. }  



使用這種方法時要特別當心,須要十分肯定c1和c2是彼此無關的域。

7.  可重入同步

還記得嗎,一個線程不能得到其餘線程所擁有的鎖。可是它能夠得到本身已經擁有的鎖。容許一個線程屢次得到同一個鎖實現了可重入同步。這裏描述了一 種同步代碼的場景,直接的或間接地,調用了一個也擁有同步代碼的方法,且兩邊的代碼使用的是同一把鎖。若是沒有這種可重入的同步機制,同步代碼則須要採起 許多額外的預防措施以防止線程阻塞本身。

8.  原子訪問

在編程過程當中,原子操做是指全部操做都同時發生。原子操做不能被中途打斷:要麼全作,要麼不作。原子操做在完成前不會有看得見的反作用。

咱們發現像c++這樣的增量表達式,並無描述原子操做。即便是很是簡單的表達式也可以定義成能被分解爲其餘操做的複雜操做。然而,有些操做你能夠定義爲原子的:

  • 對引用變量和大部分基本類型變量(除long和double以外)的讀寫是原子的。

  • 對全部聲明爲volatile的變量(包括long和double變量)的讀寫是原子的。


原子操做不會交錯,因而能夠放心使用,沒必要擔憂線程干擾。然而,這並不能徹底消除原子操做上的同步,由於內存一致性錯誤仍可能發生。使用 volatile變量能夠下降內存一致性錯誤的風險,由於對volatile變量的任意寫操做,對於後續在該變量上的讀操做創建了happens- before關係。這意味着volatile變量的修改對於其餘線程老是可見的。更重要的是,這同時也意味着當一個線程讀取一個volatile變量時, 它不只能看到該變量最新的修改,並且也能看到導致該改變發生的代碼的副效應。

使用簡單的原子變量訪問比經過同步代碼來訪問更高效,可是須要程序員更加謹慎以免內存一致性錯誤。至於這額外的付出是否值得,得看應用的大小和複雜度。

java.util.concurrent包中的一些類提供了一些不依賴同步機制的原子方法。咱們將在高級併發對象這一節中討論它們。

     

活躍度        Top              



(本部分原文地址譯文地址,譯者:李任,鄭旭東 校對:蘑菇街-小寶
一個併發應用程序能及時執行的能力稱爲活躍性。本節將介紹最多見的活躍性問題:死鎖(deadlock),以及另外兩個活躍性問題:飢餓(starvation)和活鎖(livelock)。

1.  死鎖

死鎖描述了這樣一種情景,兩個或多個線程永久阻塞,互相等待對方釋放資源。下面是一個例子。

Alphone和Gaston是朋友,都很講究禮節。禮節有一個嚴格的規矩,當你向一個朋友鞠躬時,你必須保持鞠躬的姿式,直到你的朋友有機會回鞠給你。不幸的是,這個規矩沒有算上兩個朋友相互同時鞠躬的可能。

下面的應用例子,DeadLock,模擬了這個可能性。

Java代碼

  1.    static class Friend {  

  2.         private final String name;  

  3.         public Friend(String name) {  

  4.             this.name = name;  

  5.         }  

  6.         public String getName() {  

  7.             return this.name;  

  8.         }  

  9.         public synchronized void bow(Friend bower) {  

  10.             System.out.format("%s: %s"  

  11.                 + "  has bowed to me!%n",  

  12.                 this.name, bower.getName());  

  13.             bower.bowBack(this);  

  14.         }  

  15.         public synchronized void bowBack(Friend bower) {  

  16.             System.out.format("%s: %s"  

  17.                 + " has bowed back to me!%n",  

  18.                 this.name, bower.getName());  

  19.         }  

  20.     }  

  21.   

  22.     public static void main(String[] args) {  

  23.         final Friend alphonse =  

  24.             new Friend("Alphonse");  

  25.         final Friend gaston =  

  26.             new Friend("Gaston");  

  27.         new Thread(new Runnable() {  

  28.             public void run() { alphonse.bow(gaston); }  

  29.         }).start();  

  30.         new Thread(new Runnable() {  

  31.             public void run() { gaston.bow(alphonse); }  

  32.         }).start();  

  33.     }  

  34. }  




當DeadLock運行後,兩個線程極有可能阻塞,當它們嘗試調用bowBack方法時。沒有哪一個阻塞會結束,由於每一個線程都在等待另外一個線程退出bow方法。

2.  飢餓和活鎖

飢餓和活鎖並不如死鎖通常廣泛,但它仍然是每一個併發程序設計者可能會遇到的問題。

飢餓

飢餓是指當一個線程不能正常的訪問共享資源而且不能正常執行的狀況。這一般在共享資源被其餘「貪心」的線程長期時發生。舉個例子,假設一個對象提 供了一個同步方法,這個方法一般須要執行很長一段時間才返回。若是一個線程常常調用這個方法,那麼其餘須要同步的訪問這個對象的線程就常常會被阻塞。

活鎖

一個線程一般會有會響應其餘線程的活動。若是其餘線程也會響應另外一個線程的活動,那麼就有可能發生活鎖。同死鎖同樣,發生活鎖的線程沒法繼續執 行。然而線程並無阻塞——他們在忙於響應對方沒法恢復工做。這就至關於兩個在走廊相遇的人:Alphonse向他本身的左邊靠想讓Gaston過去,而 Gaston向他的右邊靠想讓Alphonse過去。可見他們阻塞了對方。Alphonse向他的右邊靠,而Gaston向他的左邊靠,他們仍是阻塞了對 方。

     

保護塊(Guarded Blocks)        Top              


(本部分原文鏈接譯文鏈接,譯者:Greester,校對:鄭旭東)
多線程之間常常須要協同工做,最多見的方式是使用Guarded Blocks,它循環檢查一個條件(一般初始值爲true),直到條件發生變化才跳出循環繼續執行。在使用Guarded Blocks時有如下幾個步驟須要注意:

假設guardedJoy()方法必需要等待另外一線程爲共享變量joy設值才能繼續執行。那麼理論上能夠用一個簡單的條件循環來實現,但在等待過程當中guardedJoy方法不停的檢查循環條件其實是一種資源浪費。

Java代碼

  1. public void guardedJoy() {  

  2.     // Simple loop guard. Wastes  

  3.     // processor time. Don't do this!  

  4.     while(!joy) {}  

  5.     System.out.println("Joy has been achieved!");  

  6. }  



更加高效的方法是調用Object.wait將當前線程掛起,直到有另外一線程發起事件通知(儘管通知的事件不必定是當前線程等待的事件)。

Java代碼

  1. public synchronized void guardedJoy() {  

  2.     // This guard only loops once for each special event, which may not  

  3.     // be the event we're waiting for.  

  4.     while(!joy) {  

  5.         try {  

  6.             wait();  

  7.         } catch (InterruptedException e) {}  

  8.     }  

  9.     System.out.println("Joy and efficiency have been achieved!");  

  10. }  



注意:必定要在循環裏面調用wait方法,不要想固然的認爲線程喚醒後循環條件必定發生了改變。

和其餘能夠暫停線程執行的方法同樣,wait方法會拋出InterruptedException,在上面的例子中,由於咱們關心的是joy的值,因此忽略了InterruptedException。

爲何guardedJoy是synchronized方法?假設d是用來調用wait的對象,當一個線程調用d.wait,它必需要擁有d的內部鎖(不然會拋出異常),得到d的內部鎖的最簡單方法是在一個synchronized方法裏面調用wait。

當一個線程調用wait方法時,它釋放鎖並掛起。而後另外一個線程請求並得到這個鎖並調用Object.notifyAll通知全部等待該鎖的線程。

Java代碼

  1. public synchronized notifyJoy() {  

  2.     joy = true;  

  3.     notifyAll();  

  4. }  



當第二個線程釋放這個該鎖後,第一個線程再次請求該鎖,從wait方法返回並繼續執行。

注意:還有另一個通知方法,notify(),它只會喚醒一個線程。但因爲它並不容許指定哪個線程被喚醒,因此通常只在大規模併發應用(即系統有大量類似任務的線程)中使用。由於對於大規模併發應用,咱們其實並不關心哪個線程被喚醒。

如今咱們使用Guarded blocks建立一個生產者/消費者應用。這類應用須要在兩個線程之間共享數據:生產者生產數據,消費者使用數據。兩個線程經過共享對象通訊。在這裏,線 程協同工做的關鍵是:生產者發佈數據以前,消費者不可以去讀取數據;消費者沒有讀取舊數據前,生產者不能發佈新數據。

在下面的例子中,數據經過Drop對象共享的一系列文本消息:

Java代碼

  1. public class Drop {  

  2.     // Message sent from producer  

  3.     // to consumer.  

  4.     private String message;  

  5.     // True if consumer should wait  

  6.     // for producer to send message,  

  7.     // false if producer should wait for  

  8.     // consumer to retrieve message.  

  9.     private boolean empty = true;  

  10.   

  11.     public synchronized String take() {  

  12.         // Wait until message is  

  13.         // available.  

  14.         while (empty) {  

  15.             try {  

  16.                 wait();  

  17.             } catch (InterruptedException e) {}  

  18.         }  

  19.         // Toggle status.  

  20.         empty = true;  

  21.         // Notify producer that  

  22.         // status has changed.  

  23.         notifyAll();  

  24.         return message;  

  25.     }  

  26.   

  27.     public synchronized void put(String message) {  

  28.         // Wait until message has  

  29.         // been retrieved.  

  30.         while (!empty) {  

  31.             try {  

  32.                 wait();  

  33.             } catch (InterruptedException e) {}  

  34.         }  

  35.         // Toggle status.  

  36.         empty = false;  

  37.         // Store message.  

  38.         this.message = message;  

  39.         // Notify consumer that status  

  40.         // has changed.  

  41.         notifyAll();  

  42.     }  

  43. }  



Producer是生產者線程,發送一組消息,字符串DONE表示全部消息都已經發送完成。爲了模擬現實狀況,生產者線程還會在消息發送時隨機的暫停。

Java代碼

  1. import java.util.Random;  

  2.   

  3. public class Producer implements Runnable {  

  4.     private Drop drop;  

  5.   

  6.     public Producer(Drop drop) {  

  7.         this.drop = drop;  

  8.     }  

  9.   

  10.     public void run() {  

  11.         String importantInfo[] = {  

  12.             "Mares eat oats",  

  13.             "Does eat oats",  

  14.             "Little lambs eat ivy",  

  15.             "A kid will eat ivy too"  

  16.         };  

  17.         Random random = new Random();  

  18.   

  19.         for (int i = 0;  

  20.              i &lt; importantInfo.length;  

  21.              i++) {  

  22.             drop.put(importantInfo[i]);  

  23.             try {  

  24.                 Thread.sleep(random.nextInt(5000));  

  25.             } catch (InterruptedException e) {}  

  26.         }  

  27.         drop.put("DONE");  

  28.     }  

  29. }  



Consumer是消費者線程,讀取消息並打印出來,直到讀取到字符串DONE爲止。消費者線程在消息讀取時也會隨機的暫停。

Java代碼

  1. import java.util.Random;  

  2.   

  3. public class Consumer implements Runnable {  

  4.     private Drop drop;  

  5.   

  6.     public Consumer(Drop drop) {  

  7.         this.drop = drop;  

  8.     }  

  9.   

  10.     public void run() {  

  11.         Random random = new Random();  

  12.         for (String message = drop.take();  

  13.              ! message.equals("DONE");  

  14.              message = drop.take()) {  

  15.             System.out.format("MESSAGE RECEIVED: %s%n", message);  

  16.             try {  

  17.                 Thread.sleep(random.nextInt(5000));  

  18.             } catch (InterruptedException e) {}  

  19.         }  

  20.     }  

  21. }  



ProducerConsumerExample是主線程,它啓動生產者線程和消費者線程。

Java代碼

  1. public class ProducerConsumerExample {  

  2.     public static void main(String[] args) {  

  3.         Drop drop = new Drop();  

  4.         (new Thread(new Producer(drop))).start();  

  5.         (new Thread(new Consumer(drop))).start();  

  6.     }  

  7. }  



注意:Drop類是用來演示Guarded Blocks如何工做的。爲了不從新發明輪子,當你嘗試建立本身的共享數據對象時,請查看Java Collections Framework中已有的數據結構。如需更多信息,請參考Questions and Exercises

     

不可變對象        Top              



(本部分原文連接譯文連接,譯者:Greenster,校對:鄭旭東)
一個對象若是在建立後不能被修改,那麼就稱爲不可變對象。在併發編程中,一種被廣泛承認的原則就是:儘量的使用不可變對象來建立簡單、可靠的代碼。

在併發編程中,不可變對象特別有用。因爲建立後不能被修改,因此不會出現因爲線程干擾產生的錯誤或是內存一致性錯誤。

可是程序員們一般並不熱衷於使用不可變對象,由於他們擔憂每次建立新對象的開銷。實際上這種開銷經常被過度高估,並且使用不可變對象所帶來的一些 效率提高也抵消了這種開銷。例如:使用不可變對象下降了垃圾回收所產生的額外開銷,也減小了用來確保使用可變對象不出現併發錯誤的一些額外代碼。

接下來看一個可變對象的類,而後轉化爲一個不可變對象的類。經過這個例子說明轉化的原則以及使用不可變對象的好處。

一個同步類的例子

SynchronizedRGB是表示顏色的類,每個對象表明一種顏色,使用三個整形數表示顏色的三基色,字符串表示顏色名稱。

Java代碼

  1. public class SynchronizedRGB {  

  2.   

  3.     // Values must be between 0 and 255.  

  4.     private int red;  

  5.     private int green;  

  6.     private int blue;  

  7.     private String name;  

  8.   

  9.     private void check(int red,  

  10.                        int green,  

  11.                        int blue) {  

  12.         if (red < 0 || red > 255  

  13.             || green < 0 || green > 255  

  14.             || blue < 0 || blue > 255) {  

  15.             throw new IllegalArgumentException();  

  16.         }  

  17.     }  

  18.   

  19.     public SynchronizedRGB(int red,  

  20.                            int green,  

  21.                            int blue,  

  22.                            String name) {  

  23.         check(red, green, blue);  

  24.         this.red = red;  

  25.         this.green = green;  

  26.         this.blue = blue;  

  27.         this.name = name;  

  28.     }  

  29.   

  30.     public void set(int red,  

  31.                     int green,  

  32.                     int blue,  

  33.                     String name) {  

  34.         check(red, green, blue);  

  35.         synchronized (this) {  

  36.             this.red = red;  

  37.             this.green = green;  

  38.             this.blue = blue;  

  39.             this.name = name;  

  40.         }  

  41.     }  

  42.   

  43.     public synchronized int getRGB() {  

  44.         return ((red << 16) | (green << 8) | blue);  

  45.     }  

  46.   

  47.     public synchronized String getName() {  

  48.         return name;  

  49.     }  

  50.   

  51.     public synchronized void invert() {  

  52.         red = 255 - red;  

  53.         green = 255 - green;  

  54.         blue = 255 - blue;  

  55.         name = "Inverse of " + name;  

  56.     }  

  57. }  



使用SynchronizedRGB時須要當心,避免其處於不一致的狀態。例如一個線程執行了如下代碼:

Java代碼

  1. SynchronizedRGB color =  

  2.     new SynchronizedRGB(0, 0, 0, "Pitch Black");  

  3. ...  

  4. int myColorInt = color.getRGB();      //Statement 1  

  5. String myColorName = color.getName(); //Statement 2  



若是有另一個線程在Statement 1以後、Statement 2以前調用了color.set方法,那麼myColorInt的值和myColorName的值就會不匹配。爲了不出現這樣的結果,必需要像下面這樣把這兩條語句綁定到一塊執行:

Java代碼

  1. synchronized (color) {  

  2.     int myColorInt = color.getRGB();  

  3.     String myColorName = color.getName();  

  4. }  



這種不一致的問題只可能發生在可變對象上。

定義不可變對象的策略

如下的一些規則是建立不可變對象的簡單策略。並不是全部不可變類都徹底遵照這些規則,不過這不是編寫這些類的程序員們粗枝大葉形成的,極可能的是他們有充分的理由確保這些對象在建立後不會被修改。但這須要很是複雜細緻的分析,並不適用於初學者。

  1. 不要提供setter方法。(包括修改字段的方法和修改字段引用對象的方法)

  2. 將類的全部字段定義爲final、private的。

  3. 不容許子類重寫方法。簡單的辦法是將類聲明爲final,更好的方法是將構造函數聲明爲私有的,經過工廠方法建立對象。

  4. 若是類的字段是對可變對象的引用,不容許修改被引用對象。

                &middot;不提供修改可變對象的方法。
                &middot;不共享可變對象的引用。當一個引用被當作參數傳遞給構造函數,而這個引用指向的是一個外部的可變對象時,必定不要保存這個引用。若是必需要保存,那麼建立可變對象的拷貝,而後保存拷貝對象的引用。一樣若是須要返回內部的可變對象時,不要返回可變對象自己,而是返回其拷貝。

將這一策略應用到SynchronizedRGB有如下幾步:

  1. SynchronizedRGB類有兩個setter方法。第一個set方法只是簡單的爲字段設值(譯者注:刪掉便可),第二個invert方法修改成建立一個新對象,而不是在原有對象上修改。

  2. 全部的字段都已是私有的,加上final便可。

  3. 將類聲明爲final的

  4. 只有一個字段是對象引用,而且被引用的對象也是不可變對象。


通過以上這些修改後,咱們獲得了ImmutableRGB

Java代碼

  1. final public class ImmutableRGB {  

  2.   

  3.     // Values must be between 0 and 255.  

  4.     final private int red;  

  5.     final private int green;  

  6.     final private int blue;  

  7.     final private String name;  

  8.   

  9.     private void check(int red,  

  10.                        int green,  

  11.                        int blue) {  

  12.         if (red < 0 || red > 255  

  13.             || green < 0 || green > 255  

  14.             || blue < 0 || blue > 255) {  

  15.             throw new IllegalArgumentException();  

  16.         }  

  17.     }  

  18.   

  19.     public ImmutableRGB(int red,  

  20.                         int green,  

  21.                         int blue,  

  22.                         String name) {  

  23.         check(red, green, blue);  

  24.         this.red = red;  

  25.         this.green = green;  

  26.         this.blue = blue;  

  27.         this.name = name;  

  28.     }  

  29.   

  30.     public int getRGB() {  

  31.         return ((red << 16) | (green << 8) | blue);  

  32.     }  

  33.   

  34.     public String getName() {  

  35.         return name;  

  36.     }  

  37.   

  38.     public ImmutableRGB invert() {  

  39.         return new ImmutableRGB(255 - red,  

  40.                        255 - green,  

  41.                        255 - blue,  

  42.                        "Inverse of " + name);  

  43.     }  

  44. }  


     

高級併發對象        Top              



(本部分原文連接譯文連接,譯者:李任
目前爲止,該教程重點講述了最初做爲Java平臺一部分的低級別API。這些API對於很是基本的任務來講已經足夠,可是對於更高級的任務就須要 更高級的API。特別是針對充分利用了當今多處理器和多核系統的大規模併發應用程序。 本節,咱們將着眼於Java 5.0新增的一些高級併發特徵。大多數特徵已經在新的java.util.concurrent包中實現。Java集合框架中也定義了新的併發數據結構。

  • 鎖對象提供了能夠簡化許多併發應用的鎖的慣用法。

  • Executors爲加載和管理線程定義了高級API。Executors的實現由java.util.concurrent包提供,提供了適合大規模應用的線程池管理。

  • 併發集合簡化了大型數據集合管理,且極大的減小了同步的需求。

  • 原子變量有減少同步粒度和避免內存一致性錯誤的特徵。

  • 併發隨機數(JDK7)提供了高效的多線程生成僞隨機數的方法。

1.  鎖對象

同步代碼依賴於一種簡單的可重入鎖。這種鎖使用簡單,但也有諸多限制。

java.util.concurrent.locks包提供了更復雜的鎖。咱們不會詳細考察這個包,但會重點關注其最基本的接口,鎖。  鎖對象做用很是相似同步代碼使用的隱式鎖。如同隱式鎖,每次只有一個線程能夠得到鎖對象。經過關聯Condition對 象,鎖對象也支持wait/notify機制。 鎖對象之於隱式鎖最大的優點在於,它們有能力收回得到鎖的嘗試。若是當前鎖對象不可用,或者鎖請求超時(若是超時時間已指定),tryLock方法會收回 獲取鎖的請求。若是在鎖獲取前,另外一個線程發送了一箇中斷,lockInterruptibly方法也會收回獲取鎖的請求。 讓咱們使用鎖對象來解決咱們在活躍度中 見到的死鎖問題。Alphonse和Gaston已經把本身訓練成能注意到朋友什麼時候要鞠躬。咱們經過要求Friend對象在雙方鞠躬前必須先得到鎖來模擬 此次改善。下面是改善後模型的源代碼,Safelock。爲了展現其用途普遍,咱們假設Alphonse和Gaston對於他們新發現的穩定鞠躬的能力是 如此入迷,以致於他們沒法不相互鞠躬。

Java代碼

  1. import java.util.concurrent.locks.Lock;  

  2. import java.util.concurrent.locks.ReentrantLock;  

  3. import java.util.Random;  

  4.   

  5. public class Safelock {  

  6.     static class Friend {  

  7.         private final String name;  

  8.         private final Lock lock = new ReentrantLock();  

  9.   

  10.         public Friend(String name) {  

  11.             this.name = name;  

  12.         }  

  13.   

  14.         public String getName() {  

  15.             return this.name;  

  16.         }  

  17.   

  18.         public boolean impendingBow(Friend bower) {  

  19.             Boolean myLock = false;  

  20.             Boolean yourLock = false;  

  21.             try {  

  22.                 myLock = lock.tryLock();  

  23.                 yourLock = bower.lock.tryLock();  

  24.             } finally {  

  25.                 if (! (myLock && yourLock)) {  

  26.                     if (myLock) {  

  27.                         lock.unlock();  

  28.                     }  

  29.                     if (yourLock) {  

  30.                         bower.lock.unlock();  

  31.                     }  

  32.                 }  

  33.             }  

  34.             return myLock && yourLock;  

  35.         }  

  36.   

  37.         public void bow(Friend bower) {  

  38.             if (impendingBow(bower)) {  

  39.                 try {  

  40.                     System.out.format("%s: %s has"  

  41.                         + " bowed to me!%n",  

  42.                         this.name, bower.getName());  

  43.                     bower.bowBack(this);  

  44.                 } finally {  

  45.                     lock.unlock();  

  46.                     bower.lock.unlock();  

  47.                 }  

  48.             } else {  

  49.                 System.out.format("%s: %s started"  

  50.                     + " to bow to me, but saw that"  

  51.                     + " I was already bowing to"  

  52.                     + " him.%n",  

  53.                     this.name, bower.getName());  

  54.             }  

  55.         }  

  56.   

  57.         public void bowBack(Friend bower) {  

  58.             System.out.format("%s: %s has" +  

  59.                 " bowed back to me!%n",  

  60.                 this.name, bower.getName());  

  61.         }  

  62. }  

  63.   

  64.     static class BowLoop implements Runnable {  

  65.         private Friend bower;  

  66.         private Friend bowee;  

  67.   

  68.         public BowLoop(Friend bower, Friend bowee) {  

  69.             this.bower = bower;  

  70.             this.bowee = bowee;  

  71.         }  

  72.   

  73.         public void run() {  

  74.             Random random = new Random();  

  75.             for (;;) {  

  76.                 try {  

  77.                     Thread.sleep(random.nextInt(10));  

  78.                 } catch (InterruptedException e) {}  

  79.                 bowee.bow(bower);  

  80.             }  

  81.         }  

  82.     }  

  83.   

  84.     public static void main(String[] args) {  

  85.         final Friend alphonse =  

  86.             new Friend("Alphonse");  

  87.         final Friend gaston =  

  88.             new Friend("Gaston");  

  89.         new Thread(new BowLoop(alphonse, gaston)).start();  

  90.         new Thread(new BowLoop(gaston, alphonse)).start();  

  91.     }  

  92. }  



2.  執行器(Executors)

在以前全部的例子中,Thread對象表示的線程和Runnable對象表示的線程所執行的任務之間是緊耦合的。這對於小型應用程序來講沒問題, 但對於大規模併發應用來講,合理的作法是將線程的建立與管理和程序的其餘部分分離開。封裝這些功能的對象就是執行器,接下來的部分將講詳細描述執行器。 

3.  Executor接口

java.util.concurrent中包括三個Executor接口:

  • Executor,一個運行新任務的簡單接口。

  • ExecutorService,擴展了Executor接口。添加了一些用來管理執行器生命週期和任務生命週期的方法。

  • ScheduledExecutorService,擴展了ExecutorService。支持Future和按期執行任務。

一般來講,指向Executor對象的變量應被聲明爲以上三種接口之一,而不是具體的實現類。

Executor接口

Executor接口只有一個execute方法,用來替代一般建立(啓動)線程的方法。例如:r是一個Runnable對象,e是一個Executor對象。可使用

Java代碼

  1. e.execute(r);  


來代替

Java代碼

  1. (new Thread(r)).start();  


但execute方法沒有定義具體的實現方式。對於不一樣的Executor實現,execute方法多是建立一個新線程並當即啓動,但更有多是使用已有的工做線程運行r,或者將r放入到隊列中等待可用的工做線程。(咱們將在線程池一節中描述工做線程。)

ExecutorService接口

ExecutorService接 口在提供了execute方法的同時,新加了更加通用的submit方法。submit方法除了和execute方法同樣能夠接受Runnable對象做 爲參數,還能夠接受Callable對象做爲參數。使用Callable對象能夠能使任務返還執行的結果。經過submit方法返回的Future對象可 以讀取Callable任務的執行結果,或是管理Callable任務和Runnable任務的狀態。 ExecutorService也提供了批量運行Callable任務的方法。最後,ExecutorService還提供了一些關閉執行器的方法。若是 須要支持即時關閉,執行器所執行的任務須要正確處理中斷。

ScheduledExecutorService接口

ScheduledExecutorService擴 展ExecutorService接口並添加了schedule方法。調用schedule方法能夠在指定的延時後執行一個Runnable或者 Callable任務。ScheduledExecutorService接口還定義了按照指定時間間隔按期執行任務的 scheduleAtFixedRate方法和scheduleWithFixedDelay方法。

4.  線程池

在java.util.concurrent包中多數的執行器實現都使用了由工做線程組成的線程池,工做線程獨立於所它所執行的Runnable任務和Callable任務,而且經常使用來執行多個任務。 使用工做線程可使建立線程的開銷最小化。

在大規模併發應用中,建立大量的Thread對象會佔用佔用大量系統內存,分配和回收這些對象會產生很大的開銷。一種最多見的線程池是固定大小的 線程池。這種線程池始終有必定數量的線程在運行,若是一個線程因爲某種緣由終止運行了,線程池會自動建立一個新的線程來代替它。須要執行的任務經過一個內 部隊列提交給線程,當沒有更多的工做線程能夠用來執行任務時,隊列保存額外的任務。 使用固定大小的線程池一個很重要的好處是能夠實現優雅退化。例如一個Web服務器,每個HTTP請求都是由一個單獨的線程來處理的,若是爲每個 HTTP都建立一個新線程,那麼當系統的開銷超出其能力時,會忽然地對全部請求都中止響應。若是限制Web服務器能夠建立的線程數量,那麼它就沒必要當即處 理全部收到的請求,而是在有能力處理請求時才處理。 建立一個使用線程池的執行器最簡單的方法是調用java.util.concurrent.ExecutorsnewFixedThreadPool方法。Executors類還提供了下列一下方法:

  • newCachedThreadPool方法建立了一個可擴展的線程池。適合用來啓動不少短任務的應用程序。

  • newSingleThreadExecutor方法建立了每次執行一個任務的執行器。

  • 還有一些建立ScheduledExecutorService執行器的方法。

若是上面的方法都不知足須要,能夠嘗試java.util.concurrent.ThreadPoolExecutor或者java.util.concurrent.ScheduledThreadPoolExecutor

5.  Fork/Joint

fork/join框架是ExecutorService接口的一種具體實現,目的是爲了幫助你更好地利用多處理器帶來的好處。它是爲那些可以被 遞歸地拆解成子任務的工做類型量身設計的。其目的在於可以使用全部可用的運算能力來提高你的應用的性能。   相似於ExecutorService接口的其餘實現,fork/join框架會將任務分發給線程池中的工做線程。fork/join框架的獨特之處在與 它使用工做竊取(work-stealing)算法。完成本身的工做而處於空閒的工做線程可以從其餘仍然處於忙碌(busy)狀態的工做線程處竊取等待執 行的任務。 fork/join框架的核心是ForkJoinPool類,它是對AbstractExecutorService類的擴展。ForkJoinPool實現了工做偷取算法,並能夠執行ForkJoinTask任務。

基本使用方法

使用fork/join框架的第一步是編寫執行一部分工做的代碼。你的代碼結構看起來應該與下面所示的僞代碼相似:

Java代碼

  1. if (當前這個任務工做量足夠小)  

  2.     直接完成這個任務  

  3. else  

  4.     將這個任務或這部分工做分解成兩個部分  

  5.     分別觸發(invoke)這兩個子任務的執行,並等待結果  



你須要將這段代碼包裹在一個ForkJoinTask的子類中。不過,一般狀況下會使用一種更爲具體的的類型,或者是RecursiveTask(會返回一個結果),或者是RecursiveAction。 當你的ForkJoinTask子類準備好了,建立一個表明全部須要完成工做的對象,而後將其做爲參數傳遞給一個ForkJoinPool實例的invoke()方法便可。

要清晰,先模糊

想要了解fork/join框架的基本工做原理,接下來的這個例子會有所幫助。假設你想要模糊一張圖片。原始的source圖片由一個整數的數組 表示,每一個整數表示一個像素點的顏色數值。與source圖片相同,模糊以後的destination圖片也由一個整數數組表示。 對圖片的模糊操做是經過對source數組中的每個像素點進行處理完成的。處理的過程是這樣的:將每一個像素點的色值取出,與周圍像素的色值(紅、黃、藍 三個組成部分)放在一塊兒取平均值,獲得的結果被放入destination數組。由於一張圖片會由一個很大的數組來表示,這個流程會花費一段較長的時間。 若是使用fork/join框架來實現這個模糊算法,你就可以藉助多處理器系統的並行處理能力。下面是上述算法結合fork/join框架的一種簡單實 現:

Java代碼

  1. public class ForkBlur extends RecursiveAction {  

  2. private int[] mSource;  

  3. private int mStart;  

  4. private int mLength;  

  5. private int[] mDestination;  

  6.   

  7. // Processing window size; should be odd.  

  8. private int mBlurWidth = 15;  

  9.   

  10. public ForkBlur(int[] src, int start, int length, int[] dst) {  

  11.     mSource = src;  

  12.     mStart = start;  

  13.     mLength = length;  

  14.     mDestination = dst;  

  15. }  

  16.   

  17. protected void computeDirectly() {  

  18.     int sidePixels = (mBlurWidth - 1) / 2;  

  19.     for (int index = mStart; index &lt; mStart + mLength; index++) {  

  20.         // Calculate average.  

  21.         float rt = 0, gt = 0, bt = 0;  

  22.         for (int mi = -sidePixels; mi &lt;= sidePixels; mi++) {  

  23.             int mindex = Math.min(Math.max(mi + index, 0),  

  24.                                 mSource.length - 1);  

  25.             int pixel = mSource[mindex];  

  26.             rt += (float)((pixel &amp; 0x00ff0000) &gt;&gt; 16)  

  27.                   / mBlurWidth;  

  28.             gt += (float)((pixel &amp; 0x0000ff00) &gt;&gt;  8)  

  29.                   / mBlurWidth;  

  30.             bt += (float)((pixel &amp; 0x000000ff) &gt;&gt;  0)  

  31.                   / mBlurWidth;  

  32.         }  

  33.   

  34.         // Reassemble destination pixel.  

  35.         int dpixel = (0xff000000     ) |  

  36.                (((int)rt) &lt;&lt; 16) |  

  37.                (((int)gt) &lt;&lt;  8) |  

  38.                (((int)bt) &lt;&lt;  0);  

  39.         mDestination[index] = dpixel;  

  40.     }  

  41. }  



接下來你須要實現父類中的compute()方法,它會直接執行模糊處理,或者將當前的工做拆分紅兩個更小的任務。數組的長度能夠做爲一個簡單的閥值來判斷任務是應該直接完成仍是應該被拆分。

Java代碼

  1. protected static int sThreshold = 100000;  

  2.   

  3. protected void compute() {  

  4.     if (mLength &lt; sThreshold) {  

  5.         computeDirectly();  

  6.         return;  

  7.     }  

  8.   

  9.     int split = mLength / 2;  

  10.   

  11.     invokeAll(new ForkBlur(mSource, mStart, split, mDestination),  

  12.               new ForkBlur(mSource, mStart + split, mLength - split,  

  13.                            mDestination));  

  14. }  



若是前面這個方法是在一個RecursiveAction的子類中,那麼設置任務在ForkJoinPool中執行就再直觀不過了。一般會包含如下一些步驟:

(1) 建立一個表示全部須要完成工做的任務。

Java代碼

  1. // source image pixels are in src  

  2. // destination image pixels are in dst  

  3. ForkBlur fb = new ForkBlur(src, 0, src.length, dst);  



(2) 建立將要用來執行任務的ForkJoinPool。

Java代碼

  1. ForkJoinPool pool = new ForkJoinPool();  



(3) 執行任務。

Java代碼

  1. pool.invoke(fb);  



想要瀏覽完成的源代碼,請查看ForkBlur,其中還包含一些建立destination圖片文件的額外代碼。

標準實現

除了可以使用fork/join框架來實現可以在多處理系統中被並行執行的定製化算法(如前文中的ForkBlur.java例子),在Java SE中一些比較經常使用的功能點也已經使用fork/join框架來實現了。在Java SE 8中,java.util.Arrays類的一系列parallelSort()方法就使用了fork/join來實現。這些方法與sort()系列方法 很相似,可是經過使用fork/join框架,藉助了併發來完成相關工做。在多處理器系統中,對大數組的並行排序會比串行排序更快。這些方法到底是如何運 用fork/join框架並不在本教程的討論範圍內。想要了解更多的信息,請參見Java API文檔。 其餘採用了fork/join框架的方法還包括java.util.streams包中的一些方法,此包是做爲Java SE 8發行版中Project Lambda的一部分。想要了解更多信息,請參見Lambda Expressions一節。

6.  併發集合

java.util.concurrent包囊括了Java集合框架的一些附加類。它們也最容易按照集合類所提供的接口來進行分類:

  • BlockingQueue定義了一個先進先出的數據結構,當你嘗試往滿隊列中添加元素,或者從空隊列中獲取元素時,將會阻塞或者超時。

  • ConcurrentMapjava.util.Map的子接口,定義了一些有用的原子操做。移除或者替換鍵值對的操做只有當key存在時才能進行,而新增操做只有當key不存在時。使這些操做原子化,能夠避免同步。ConcurrentMap的標準實現是ConcurrentHashMap,它是HashMap的併發模式。

  • ConcurrentNavigableMap是ConcurrentMap的子接口,支持近似匹配。ConcurrentNavigableMap的標準實現是ConcurrentSkipListMap,它是TreeMap的併發模式。

  • 全部這些集合,經過 在集合裏新增對象和訪問或移除對象的操做之間,定義一個happens-before的關係,來幫助程序員避免內存一致性錯誤


7.  原子變量

java.util.concurrent.atomic包 定義了對單一變量進行原子操做的類。全部的類都提供了get和set方法,可使用它們像讀寫volatile變量同樣讀寫原子類。就是說,同一變量上的 一個set操做對於任意後續的get操做存在happens-before關係。原子的compareAndSet方法也有內存一致性特色,就像應用到整 型原子變量中的簡單原子算法。   爲了看看這個包如何使用,讓咱們返回到最初用於演示線程干擾的Counter類:

Java代碼

  1. class Counter {  

  2.     private int c = 0;  

  3.     public void increment() {  

  4.         c++;  

  5.     }  

  6.   

  7.     public void decrement() {  

  8.         c--;  

  9.     }  

  10.   

  11.     public int value() {  

  12.         return c;  

  13.     }  

  14. }  



使用同步是一種使Counter類變得線程安全的方法,如SynchronizedCounter

Java代碼

  1. class SynchronizedCounter {  

  2. private int c = 0;  

  3. public synchronized void increment() {  

  4. c++;  

  5. }  

  6. public synchronized void decrement() {  

  7. c--;  

  8. }  

  9. public synchronized int value() {  

  10. return c;  

  11. }  

  12. }  



對於這個簡單的類,同步是一種可接受的解決方案。可是對於更復雜的類,咱們可能想要避免沒必要要同步所帶來的活躍度影響。將int替換爲AtomicInteger容許咱們在不進行同步的狀況下阻止線程干擾,如AtomicCounter

Java代碼

  1. import java.util.concurrent.atomic.AtomicInteger;  

  2. class AtomicCounter {  

  3. private AtomicInteger c = new AtomicInteger(0);  

  4. public void increment() {  

  5. c.incrementAndGet();  

  6. }  

  7.   

  8. public void decrement() {  

  9. c.decrementAndGet();  

  10. }  

  11.   

  12. public int value() {  

  13. return c.get();  

  14. }  



8.  併發隨機數

在JDK7中,java.util.concurrent包含了一個至關便利的類,ThreadLocalRandom,當應用程序指望在多個線程或ForkJoinTasks中使用隨機數時。

對於併發訪問,使用TheadLocalRandom代替Math.random()能夠減小競爭,從而得到更好的性能。

你只需調用ThreadLocalRandom.current(), 而後調用它的其中一個方法去獲取一個隨機數便可。下面是一個例子:

Java代碼

  1. int r = ThreadLocalRandom.current().nextInt(4,77); 

相關文章
相關標籤/搜索