最近在看《java並發編程實戰》,希望自己有毅力把它讀完。
線程本身有很多優勢,比如可以發揮多處理器的強大能力、建模更加簡單、簡化異步事件的處理、使用戶界面的相應更加靈敏,但是更多的需要程序猿面對的是安全性問題。看下面例子:
public class UnsafeSequence { private int value; /*返回一個唯一的數值*/ public int getNext(){ return value++; } }
UnsafeSequence的問題在於,如果執行時機不對,那么兩個線程在調用getNext時會得到相同的值,圖1給出了這種錯誤情況。雖然遞增運算value++看上去是單個操作,但事實上它包含三個獨立的操作: 讀取value、將value加1、將計算結果寫入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提供了鎖機制來解決這一問題,但這些終歸只是一些機制,要編寫線程安全的代碼,其核心在於要對對象的狀態進行管理。
對象的狀態是指存儲在狀態變量(例如實例或者靜態域)中的數據。
一、線程封閉
如果一個對象無狀態,它一定是線程安全的。
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(); }
二、用鎖來保護狀態
1.內置鎖
java提供了一種內置的鎖機制來支持原子性:同步代碼塊。同步代碼塊包括兩部分:一個作為鎖的對象引用,一個作為由這個鎖保護的代碼塊。以關鍵字synchronized來修飾的方法就是一種橫跨整個方法體的同步代碼塊,其中該同步代碼塊的鎖就是方法調用所在的對象,一般不要這么做,這樣會影響效率。
synchronized(lock){
//訪問或修改由鎖保護的共享狀態
}
每一個java對象都可以用作一個實現同步的鎖,這些鎖被稱為內置鎖或者監視器鎖。線程在進入同步代碼塊之前會自動獲得鎖,並且在退出同步代碼塊時自動釋放鎖。
對象的內置鎖與其狀態之間沒有內在的關聯。雖然大多數類都將內置鎖用做一種有效的加鎖機制,但對象的域不一定要通過內置鎖來保護。當獲取與對象關聯的鎖時,並不能阻止其他線程訪問該對象,某個線程在獲得對象的鎖以后,只能阻止其他線程獲得同一個鎖。之所以每個對象都有一個內置鎖,只是為了免去顯式的創建鎖對象。
開發中常見的內置鎖的使用方法是,將所有的可變狀態都封裝在對象內部,並通過對象的內置鎖對所有訪問可變狀態餓代碼路徑進行同步,使得在該對象上不會發生並發訪問,例如,Vector和其他的同步集合類。
2.Volatile變量
同步還有另外一層意思:我們不僅希望防止某個線程正在使用對象狀態而另一個線程正在同時修改該狀態,而且希望確保當一個線程修改了對象狀態后,其他線程能夠看到發生的狀態變化。java提供了一種削弱的同步機制,即volatile變量,用來確保將變量的更新操作通知其他線程。
volatile變量的典型用法:
volatile boolean asleep; ... while(!asleep){ ... }
volatile變量通常用做某個操作完成,發生中斷或者作為狀態的標志。volatile的語義不足以確保遞增操作的原子性。也就是說,加鎖機制既可以確保可見性又可以確保原子性,而volatile變量只能確保可見性。
關於volatile后補:
這塊的講解不是很詳細,這里重新整理下,首先要達成一個共識:
1、每個線程都有自己的線程存儲空間
2、線程何時同步本地存儲空間的數據到主存是不確定的。
正是由於這種不確定性,一個線程修改了數據其他線程不能及時看到,而使用volatile以后,做了如下事情
1、每次修改volatile變量都會同步到主存中
2、每次讀取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; } }
三,不可變對象
如果一個對象在被創建后其狀態就不能被修改,那么這個對象是不可變對象,所以,不可變指的是狀態不可變。不可變對象一定是線程安全的。
書中給出了一個判斷不可變對象的原則:
- 對象創建以后其狀態不能修改(聽着像廢話)
- 對象的所有域都是final類型
- 對象是正確創建的(在對象的創建期間,this引用沒有逸出)
例子:
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)可變對象:線程不安全,狀態可變
總之,這部分的核心是理解對象的狀態。