Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。java
線程容許多個活動同時進行。 併發編程比單線程編程更難,由於更多的事情可能會出錯,而且失敗很難重現。 你沒法避免併發。 它是平臺中固有的,也是要從多核處理器得到良好性能的要求,如今無處不在。本章包含的建議可幫助你編寫清晰,正確,文檔完備的併發程序。git
synchronized關鍵字確保一次只有一個線程能夠執行一個方法或代碼塊。許多程序員認爲同步只是一種互斥的方法,以防止一個線程在另外一個線程修改對象時看到對象處於不一致的狀態。在這個觀點中,對象以一致的狀態建立(條目 17),並由訪問它的方法鎖定。這些方法觀察狀態,並可選地引發狀態轉換,將對象從一個一致的狀態轉換爲另外一個一致的狀態。正確使用同步能夠保證沒有任何方法會觀察處處於不一致狀態的對象。程序員
這種觀點是正確的,但它只說明瞭一部分意義。若是沒有同步,一個線程的更改可能對其餘線程不可見。同步不只阻止線程觀察處於不一致狀態的對象,並且確保每一個進入同步方法或塊的線程都能看到由同一鎖保護的全部以前修改的效果。github
語言規範保證讀取或寫入變量是原子性(atomic)的,除非變量的類型是long或double [JLS, 17.4, 17.7]。換句話說,讀取long或double之外的變量,能夠保證返回某個線程存儲到該變量中的值,即便多個線程在沒有同步的狀況下同時修改變量也是如此。編程
你可能據說過,爲了提升性能,在讀取或寫入原子數據時應該避免同步。這種建議大錯特錯。雖然語言規範保證線程在讀取屬性時不會看到任意值,但它不保證由一個線程編寫的值對另外一個線程可見。同步是線程之間可靠通訊以及互斥所必需的。這是語言規範中稱之爲內存模型(memory model)的一部分,它規定了一個線程所作的更改什麼時候以及如何對其餘線程可見[JLS, 17.4;Goetz06, 16)。安全
即便數據是原子可讀和可寫的,未能同步對共享可變數據的訪問的後果也是可怕的。 考慮從另外一個線程中止一個線程的任務。 Java類庫提供了Thread.stop方法,可是這個方法好久之前就被棄用了,由於它本質上是不安全的——它的使用會致使數據損壞。 不要使用Thread.stop。 從另外一個線程中中止一個線程的推薦方法是讓第一個線程輪詢一個最初爲false的布爾類型的屬性,可是第二個線程能夠設置爲true以指示第一個線程要自行中止。 由於讀取和寫入布爾屬性是原子的,因此一些程序員在訪問屬性時不須要同步:併發
// Broken! - How long would you expect this program to run? public class StopThread { private static boolean stopRequested; public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(() -> { int i = 0; while (!stopRequested) i++; }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); stopRequested = true; } }
你可能但願這個程序運行大約一秒鐘,以後主線程將stoprequired設置爲true,從而致使後臺線程的循環終止。然而,在個人機器上,程序永遠不會終止:後臺線程永遠循環!框架
問題是在沒有同步的狀況下,沒法確保後臺線程什麼時候(若是有的話)看到主線程所作的stopRequested值的變化。 在沒有同步的狀況下,虛擬機將下面代碼:oop
while (!stopRequested) i++;
轉換成這樣:性能
if (!stopRequested) while (true) i++;
這種優化稱爲提高(hoisting,它正是OpenJDK Server VM所作的。 結果是活潑失敗( liveness failure):程序沒法取得進展。 解決問題的一種方法是同步對stopRequested屬性的訪問。 正如預期的那樣,該程序大約一秒鐘終止:
// Properly synchronized cooperative thread termination public class StopThread { private static boolean stopRequested; private static synchronized void requestStop() { stopRequested = true; } private static synchronized boolean stopRequested() { return stopRequested; } public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(() -> { int i = 0; while (!stopRequested()) i++; }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); requestStop(); } }
注意,寫方法(requestStop)和讀方法(stop- required)都是同步的。僅同步寫方法是不夠的!除非讀和寫操做同步,不然不能保證同步工做。有時,只同步寫(或讀)的程序可能在某些機器上顯示有效,但在這種狀況下,表面的現象是具備欺騙性的。
即便沒有同步,StopThread中同步方法的操做也是原子性的。換句話說,這些方法上的同步僅用於其通訊效果,而不是互斥。雖然在循環的每一個迭代上同步的成本很小,可是有一種正確的替代方法,它不那麼冗長,並且性能可能更好。若是stoprequest聲明爲volatile,則能夠省略StopThread的第二個版本中的鎖定。雖然volatile修飾符不執行互斥,但它保證任何讀取屬性的線程都會看到最近寫入的值:
// Cooperative thread termination with a volatile field public class StopThread { private static volatile boolean stopRequested; public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(() -> { int i = 0; while (!stopRequested) i++; }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); stopRequested = true; } }
在使用volatile時必定要當心。考慮下面的方法,該方法應該生成序列號:
// Broken - requires synchronization! private static volatile int nextSerialNumber = 0; public static int generateSerialNumber() { return nextSerialNumber++; }
該方法的目的是保證每次調用都返回一個惟一值(只要調用次數不超過232次)。 方法的狀態由單個可原子訪問的屬性nextSerialNumber組成,該屬性的全部可能值都是合法的。 所以,不須要同步來保護其不變量。 可是,若是沒有同步,該方法將沒法正常工做。
問題是增量運算符(++)不是原子的。 它對nextSerialNumber屬性執行兩個操做:首先它讀取值,而後它寫回一個新值,等於舊值加1。 若是第二個線程在線程讀取舊值並寫回新值之間讀取屬性,則第二個線程將看到與第一個線程相同的值並返回相同的序列號。 這是安全性失敗(safety failure):程序計算錯誤的結果。
修復generateSerialNumber的一種方法是將synchronized修飾符添加到其聲明中。 這確保了多個調用不會交叉讀取,而且每次調用該方法都會看到全部先前調用的效果。 完成後,能夠而且應該從nextSerialNumber中刪除volatile修飾符。 要保護該方法,請使用long而不是int,或者在nextSerialNumber即將包裝時拋出異常。
更好的是,遵循條目 59條中建議並使用AtomicLong類,它是java.util.concurrent.atomic包下的一部分。 這個包爲單個變量提供了無鎖,線程安全編程的基本類型。 雖然volatile只提供同步的通訊效果,但這個包還提供了原子性。 這正是咱們想要的generateSerialNumber,它可能強於同步版本的代碼:
// Lock-free synchronization with java.util.concurrent.atomic private static final AtomicLong nextSerialNum = new AtomicLong(); public static long generateSerialNumber() { return nextSerialNum.getAndIncrement(); }
避免此條目中討論的問題的最佳方法是不共享可變數據。 共享不可變數據(條目 17)或根本不共享。 換句話說,將可變數據限制在單個線程中。 若是採用此策略,則必須對其進行文檔記錄,以便在程序發展改進時維護此策略。 深刻了解正在使用的框架和類庫也很重要,由於它們可能會引入你不知道的線程。
一個線程能夠修改一個數據對象一段時間後,而後與其餘線程共享它,只同步共享對象引用的操做。而後,其餘線程能夠在不進一步同步的狀況下讀取對象,只要再也不次修改該對象。這些對象被認爲是有效不可變的( effectively immutable)[Goetz06, 3.5.4]。將這樣的對象引用從一個線程轉移到其餘線程稱爲安全發佈(safe publication )[Goetz06, 3.5.3]。安全地發佈對象引用的方法有不少:能夠將它保存在靜態屬性中,做爲類初始化的一部分;也能夠將其保存在volatile屬性、final屬性或使用正常鎖定訪問的屬性中;或者能夠將其放入併發集合中(條目 81)。
總之,當多個線程共享可變數據時,每一個讀取或寫入數據的線程都必須執行同步。 在沒有同步的狀況下,沒法保證一個線程的更改對另外一個線程可見。 未能同步共享可變數據的代價是活性失敗和安全性失敗。 這些失敗是最難調試的。 它們能夠是間歇性的和時間相關的,而且程序行爲可能在不一樣VM之間發生根本的變化。若是隻須要線程間通訊,而不須要互斥,那麼volatile修飾符是一種可接受的同步形式,可是正確使用它可能會比較棘手。