可見性##
上一章中我們討論過synchronized塊可以阻塞執行以及確保操作執行中的原子化。因而往往存在這樣一個誤區,synchronized僅僅用來對操作進行原子化,設置操作執行的邊界點。然而synchronized塊還有一個重要的作用,內存可見性。簡單的理解,即一個線程修改了對象的狀態,其他線程能夠真正地看到狀態的改變。
過期數據###
當多個線程同時訪問共享數據時,如果對共享數據的操作沒有同步,可能會出現一個線程獲取共享數據的同時另一個線程在修改共享數據,這樣第一個線程拿到的共享數據可能就會是過期的數據,也就不是最新鮮的數據。過期數據導致的錯誤可能只是輸出錯誤,但也可能嚴重到導致程序終止。
鎖和可見性###
內置鎖可以確保一個線程以某種可預見的方式看到另一個線程對於共享狀態的影響。
線程A執行某一同步塊時,線程B也進入被同一鎖監視的同步塊,這是可以保證在A釋放鎖前對A可見的變量,B獲得鎖后同樣也是可見的。
但是如果沒有同步,就沒有這樣的內存可見性的保證。因此鎖不僅僅是用於同步和互斥的,也是用來保證內存可見性的。為了保證所有線程都能看到共享數據的最新值,讀取和寫入操作都需要使用同一個鎖進行同步。
volatile###
java提供了另一種保證內存可見性的方式,即volatile變量。當一個變量被volatile修飾后,編譯器和進行時會一直監視變量的變化,且該變量不會被進行重排序。
但需要強調的是volatile不能保證原子性,即使是自增操作也保證不了。因而正確使用volatile變量的方式是用來標示狀態或生命周期事件。下面是標准的使用volatile的一些前提:
- 寫入變量並不依賴變量的當前值,或者確保只有單一線程修改變量的值
- 變量不需要和其他變量參與不變約束
- 訪問變量時沒有其他原因需要加鎖
記住:加鎖可以保證可見性和原子性,但volatile只能保證可見性。
發布和逸出##
發布是指將讓一個對象在當前范圍之外被訪問。當前范圍可大可小,可以是一個模塊,也可以是一個jar包。有時我們需要將一個對象發布出去,以供外部使用,但如果對象在未准備好時就被發布出去,是相當危險的,我們稱之為逸出。
最常見的對象發布方式是將對象的引用存儲到公共靜態域中。任何類和線程都能看到它。
public static Set<String> set;
public void initialize(){
set = new HashSet<String>();
}
還可以通過非私有的方法發布對象。
private String[] states = new String[]{
"A","B"
};
public String[] getStates() {return states;}
這樣的方式發布states會讓任何一個調用者都能修改它的內容,states域本身應該是不允許改變的。
要知道發布一個對象,意味着也發布了該對象中所有非私有域所引用的對象,甚至那些非私有域的引用鏈以及方法調用鏈中可獲得的對象都會被發布。
一旦將不該發布的對象逸出了,就會存在被誤用的風險。就像你在某個網站的用戶名密碼被黑客竊取公布到網上了,無論是否有人使用你的帳號,你的帳號已經存在風險了。
最后一種發布對象的方式是發布一個內部類實例。
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener(){
public void onEvent(Event e) {
//ThisEscape.this.toString();
//doSomething(e);
}
}
);
}
}
當ThisEscape發布EventListener時,它也無條件地發布了封裝ThisEscape的實例。因為內引類(inner class instances)的實例包含了對封裝實例隱含的引用,即this的引用。
上面的ThisEscape也演示了一種重要的逸出特例,this引用在構造時逸出。對象只用通過構造函數返回后才是穩定的狀態,因此在構造函數內部發布的對象,只是一個未完成的、不穩定對象。
不要讓this引用在構造期間逸出。
一個導致this引用在構造期間逸出的常見錯誤就是在構造函數中啟動一個線程。當對象在構造函數中創建了一個線程時,無論顯式的(通過參數傳給構造函數)還是隱式的(Thead或Runnable是所屬對象的內部類),this引用幾乎總是被新線程共享。
解決辦法是在構造函數中創建線程,但不立即啟動它。而是通過發布一個start方法來啟動線程。
線程封閉##
我們知道訪問共享的可變的數據需要同步。如果數據僅在單線程中被訪問,就不需要任何同步。因而我們可以將對象封閉在一個線程中,即可實現線程安全的目的。我們稱之為線程封閉。
Ad-hoc線程限制###
Ad-hoc(非正式的)線程限制是指維護線程限制性的任務全部落在實現上的情況。這種方式是非常容易出錯的。
其中一個特例是通過單一線程寫入共享的volatile變量,在執行“讀-改-寫”操作時是安全的。
如果可能的話,盡量用另一種線程限制的強形式(棧限制或ThreadLocal)。
棧限制###
將對象設置為本地變量,只存在於執行線程棧,其他線程無法訪問這個棧,稱之為棧限制。就算使用非線程安全的對象仍可以保證線程安全性。但一旦對象在某種情況下發布,就會導致對象逸出。
ThreadLocal###
ThreadLocal是維護線程限制的更規范的方式。它為每個使用它的線程維護一份單獨的copy,通過它提供的get和set方法對當前線程中維護的變量進行讀取和更新。
比如我們想維護一個全局的數據庫連接,這個Connection在啟動時已經初始化。因為JDBC規范並未要求Connection一定是線程安全的,在沒有額外的輔助下,使用全局的Connection不是線程安全的。此時可以利用ThreadLocal存儲Connection,從而使每個線程都有自己的Connection。
private static ThreadLocal<Connection> connectionHolder =
new ThreadLocal<Connection>(){
protected Connection initialValue() {
return DriverManager.getConnection(DB_URL);
};
};
public static Connection getConnection(){
return connectionHolder.get();
}
線程首次調用ThreadLocal.get()方法時,會請求initialValue()方法提供一個初始值。
ThreadLocal很容易被濫用。比如將它們所封閉的屬性作為使用全局變量的許可證。ThreadLocal變量會降低重入性,但會引入隱晦的類之間的耦合。因此要謹慎地使用。
不可變性##
創建后不能被修改的對象稱為不可變對象。不可變對象永遠是線程安全的。
不可變性不能簡單地等於將對象中的所有域都聲明為final類型。因為final域可以獲得一個到可變對象的引用。只有滿足如下狀態,一個對象才是不可變的:
- 它的狀態不能在創建后再被修改
- 所有域都是final類型
- 它被正確地創建(沒有發生this引用)
由於程序的狀態自始至終都是變化着的。你會覺得使用不可變對象會有很多限制。但存儲在不可變對象中的狀態可以通過替換一個新的狀態的不可變對象進行更新。
final域###
final域是不可修改的(其指向的對象是可變的),final域限制了對象的可變性,使得安全性能夠提高。
Effective Java中說道
將所有的域聲明為私有的,除非它們需要更高的可見性。
將所有的域聲明為final型,除非它們是可變的。
安全發布##
在並發程序中,使用共享對象的一些最有效的策略:
- 線程限制:一個線程限制的對象,被線程獨占,且只能被占有它的線程修改。
- 共享只讀:一個共享的只讀對象,可以被多個線程並發訪問,但任何線程都不能修改它。
- 共享線程安全:一個線程安全的對象在內部進行同步,其他線程就無需額外同步,可以通過公共接口隨意訪問。
- 被守護的:一個被守護的對象只能通過特定的鎖來訪問。
下一章我們來學習jdk提供的一些基礎的並發容器和工具。