最近在看《java併發編程實戰》,但願本身有毅力把它讀完。java
線程自己有不少優點,好比能夠發揮多處理器的強大能力、建模更加簡單、簡化異步事件的處理、使用戶界面的相應更加靈敏,可是更多的須要程序猿面對的是安全性問題。看下面例子:編程
public class UnsafeSequence { private int value; /*返回一個惟一的數值*/ public int getNext(){ return value++; } }
UnsafeSequence的問題在於,若是執行時機不對,那麼兩個線程在調用getNext時會獲得相同的值,圖1給出了這種錯誤狀況。雖然遞增運算value++看上去是單個操做,但事實上它包含三個獨立的操做: 讀取value、將value加一、將計算結果寫入value。因爲運行時可能將多個線程之間的操做交替執行,所以這兩個線程可能同時執行讀操做,從而使它們獲得相同的值,並都將這個值加1。結果就是,在不一樣線程的調用中返回了相同的值。緩存
在UnsafeSequence中說明的是一種常見的併發安全問題,稱爲競態條件。當某個計算的正確性取決於多個線程的交替執行時序時,那麼就會發生競態條件。安全
再舉個例子,延遲初始化中的競態條件:多線程
public class LazyInitRace { private HashMap<String, String> instance = null; public HashMap<String, String> getInstance(){ if (instance == null) { instance = new HashMap<String, String>(); } return instance; } }
在LazyInitRace中包含一個競態條件,它可能會破壞這個類的正確性。假定線程A和線程B同時執行getInstance,A看到instance爲空,於是建立一個新的HashMap實體,B一樣須要判斷instance是否爲空,此時的instance是否爲空,要取決於不可預測的時序,若是當B檢查時,instance也爲空,那麼在兩次調用getInstance時可能會獲得不一樣的結果,即便getInstance一般被認爲是返回相同的實例。併發
java提供了鎖機制來解決這一問題,但這些終歸只是一些機制,要編寫線程安全的代碼,其核心在於要對對象的狀態進行管理。less
對象的狀態是指存儲在狀態變量(例如實例或者靜態域)中的數據。異步
1、線程封閉
jvm
若是一個對象無狀態,它必定是線程安全的。 函數
public class StatelessServlet implements Servlet { public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { int i = 1; i++; ... } }
與大多數servlet相同,StatelessServlet是無狀態的:它即不包含任何域,也不包含任何對其餘類中域的引用。計算過程當中的臨時狀態僅存在於線程棧上的局部變量中(這塊須要對jvm內存分配有基礎瞭解),而且只能由正在執行的線程訪問。線程之間沒有共享狀態,因爲線程訪問無狀態對象的行爲並不會影響其餘線程中操做的正確性,所以無狀態對象是線程安全的。
像上面的例子,僅在線程內訪問數據,天然也就安全,這種技術稱爲線程封閉。java提供了一些機制來實現線程封閉,例如局部變量(上面的例子)和ThreadLocal類。
1.棧封閉
也就是局部變量,這塊要理解爲何局部變量是線程安全的。jvm運行時的數據分配如圖2所示。
java虛擬機棧是線程私有的,它的生命週期和線程相同。虛擬機棧描述的是java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀,用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。這部分會拋出兩種異常:若是線程請求的棧深度大於虛擬機容許的棧深度,拋出StackOverflowError;若是棧擴展時沒法申請到足夠內存,拋出OutOfMemoryError異常。
2.ThreadLocal類
ThreadLocal對象一般用於防止對可變的單實例變量或者全局變量進行共享。例如JDBC的Connection對象,JDBC並不要求Connection對象必須是線程安全的。僞代碼以下:
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){ public Connection initialValue() { return DriverManager.getConnection(DB_URL); }; }; public static Connection getConnection(){ return connectionHolder.get(); }
2、用鎖來保護狀態
1.內置鎖
java提供了一種內置的鎖機制來支持原子性:同步代碼塊。同步代碼塊包括兩部分:一個做爲鎖的對象引用,一個做爲由這個鎖保護的代碼塊。以關鍵字synchronized來修飾的方法就是一種橫跨整個方法體的同步代碼塊,其中該同步代碼塊的鎖就是方法調用所在的對象,通常不要這麼作,這樣會影響效率。
synchronized(lock){
//訪問或修改由鎖保護的共享狀態
}
每個java對象均可以用做一個實現同步的鎖,這些鎖被稱爲內置鎖或者監視器鎖。線程在進入同步代碼塊以前會自動得到鎖,而且在退出同步代碼塊時自動釋放鎖。
對象的內置鎖與其狀態之間沒有內在的關聯。雖然大多數類都將內置鎖用作一種有效的加鎖機制,但對象的域不必定要經過內置鎖來保護。當獲取與對象關聯的鎖時,並不能阻止其餘線程訪問該對象,某個線程在得到對象的鎖之後,只能阻止其餘線程得到同一個鎖。之因此每一個對象都有一個內置鎖,只是爲了免去顯式的建立鎖對象。
開發中常見的內置鎖的使用方法是,將全部的可變狀態都封裝在對象內部,並經過對象的內置鎖對全部訪問可變狀態餓代碼路徑進行同步,使得在該對象上不會發生併發訪問,例如,Vector和其餘的同步集合類。
2.Volatile變量
同步還有另一層意思:咱們不只但願防止某個線程正在使用對象狀態而另外一個線程正在同時修改該狀態,並且但願確保當一個線程修改了對象狀態後,其餘線程可以看到發生的狀態變化。java提供了一種削弱的同步機制,即volatile變量,用來確保將變量的更新操做通知其餘線程。
volatile變量的典型用法:
volatile boolean asleep; ... while(!asleep){ ... }
volatile變量一般用作某個操做完成,發生中斷或者做爲狀態的標誌。volatile的語義不足以確保遞增操做的原子性。也就是說,加鎖機制既能夠確保可見性又能夠確保原子性,而volatile變量只能確保可見性。
關於volatile後補:
這塊的講解不是很詳細,這裏從新整理下,首先要達成一個共識:
一、每一個線程都有本身的線程存儲空間
二、線程什麼時候同步本地存儲空間的數據到主存是不肯定的。
正是因爲這種不肯定性,一個線程修改了數據其餘線程不能及時看到,而使用volatile之後,作了以下事情
一、每次修改volatile變量都會同步到主存中
二、每次讀取volatile變量的值都強制從主存讀取最新的值(強制JVM不可優化volatile變量,如JVM優化後變量讀取會使用cpu緩存而不從主存中讀取)
經過直接讀取主存保證了可見性,不管哪一個線程讀取volatile類型變量都是最新數據。可是這不意味着volatile修飾的變量是線程安全的,多線程交替執行仍是會存在數據不一致的問題。看到某個成員變量被修飾成volatile類型,能夠理解爲下面代碼的行爲:
public class SynchronizedInteger{ private int value; public synchronized int getValue() { return value; } public synchronized void setValue(int value) { this.value = value; } }
三,不可變對象
若是一個對象在被建立後其狀態就不能被修改,那麼這個對象是不可變對象,因此,不可變指的是狀態不可變。不可變對象必定是線程安全的。
書中給出了一個判斷不可變對象的原則:
例子:
public final class ThreeStooges { private final Set<String> stooges = new HashSet<String>(); public ThreeStooges(){ stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); } public boolean isStooge(String name){ return stooges.contains(name); } }
儘管保存姓名的Set對象是可變的,但從ThreeStooges的設計中能夠看到,在Set對象構造完成後沒法對其進行修改。stooges是一個final類型的引用變量,所以全部的對象狀態都經過一個final域來訪問,最後一個要求是「正確的構造對象」,這個要求很容易知足,由於構造函數能使該引用由除了構造函數及其調用者以外的代碼來訪問。
至此,區分3個概念:
(1)無狀態對象:無成員變量,必定線程安全
(2)不可變對象:必定線程安全,有狀態,但狀態不可變
(3)可變對象:線程不安全,狀態可變
總之,這部分的核心是理解對象的狀態。