84
技術社區[雲棲]
對象的共享(第三章)
對象的共享
1.可見性
在多線程程序中,我們不僅希望防止某個線程正在使用對象狀態而另一個線程在同時修改該狀態,而且希望確保當一個線程修改了對象狀態後,其他線程能夠看到發生的狀態變化。如果沒有同步,那麼這種情況就無法實現。
-
重排序:在沒有同步的情況下,編譯器、處理器以及運行時都可能對操作的執行順序進行一些意想不到的調整。在缺乏足夠同步的多線程程序中,要想對內存操作的執行順序進行判斷,幾乎無法得出正確地結論。
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { @Override public void run() { while (!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }
在上麵代碼中,結果可能會輸出0。因為在缺少同步的情況下,Java內存模型允許編譯器對操作順序進行重排序,並將數值緩存在寄存器中,它還允許CPU對操作順序進行重排序,並將數值緩存在特定的緩存中。
非原子類的64位操作
Java內存模型要求,變量的讀取操作和寫入操作都必須是原子操作,但對於非volatile類型的long和double變量,JVM允許將64位的讀操作或寫操作分解為兩個32位的操作,從而破壞了原子性,除非用關鍵字“volatile”來聲明它們或者使用鎖來保護它們。內置鎖可以用於確保某個線程以一種可預測的方式來查看另一個線程的執行結果。
加鎖的含義不僅僅局限於互斥行為,還包括內存可見性,為了確保所有線程都能看到共享變量的最新值,所有執行讀操作或寫操作的線程都必須在同一個鎖上同步。-
volatile變量
Java提供了一種稍弱的同步機製,即volatile變量,用來確保將變量的更新操作通知到其他線程。當把變量聲明為volatile類型後,編譯器與jre都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。
但volatile並不會加鎖,因此也就不會產生阻塞行為。所以,volatile變量是一種比synchronized更輕量級的同步機製。
volatile變量通常用作某個操作完成、發生中斷或者正太改變的標誌,在使用時應非常小心,例如,volatile的語義不足以確保遞增(++)操作的原子性。加鎖機製既可以確保可見性又可以確保原子性,而volatile變量隻能確保可見性。
使用volatile變量的時機:
1. 對變量的寫入操作不依賴變量的當前值(避免競態條件),或者你能確保隻有單個線程更新變量的值
2. 該變量不會與其他狀態變量一起納入不變性條件中
3. 在訪問變量時不需要加鎖
2.發布與逸出
- 發布(Publish)一個對象:**使對象能夠在當前作用域之外的代碼中使用。**
- 當發布一個對象時,在該對象的非私有域中引用的所有對象同樣會被發布
- 當發布某個對象時,可能會間接發布其他對象,如發布一個List,包含在這個List中的對象也會被發布,如下代碼所示
class UnsafeStates { private String[] states = new String[] {"AK","AL"...}; public String[] getStates() {return states;} }
-
逸出(Escape):一個不該被發布的對象被發布
- 不要在構造過程中使this引用逸出
當且僅當對象的構造函數返回時,對象才處於可預測的和一致的狀態,因此,當對象從構造函數中發布時(如返回一個匿名內部類),隻是發布了一個尚未構造完成的對象。這造成了不正確構造。
常見的使this逸出的操作:
1. 在構造函數中啟動一個線程
如果想在構造函數中注冊一個事件監聽器或啟動線程,可以使用一個私有的構造函數和一個公共的工廠方法
```
class SafeListener {
private final EventListener listener;
private SafeListener {
listener = new EventLIstener() {
public void onEvent(Event e) {doSomething(e);
};
}
static public SafeListener newInstance(EventSource source) {
...//構造、返回
}
} 2. 在構造函數中調用一個可改寫的實例方法(既不是私有方法,也不是final方法)
- 不要在構造過程中使this引用逸出
當且僅當對象的構造函數返回時,對象才處於可預測的和一致的狀態,因此,當對象從構造函數中發布時(如返回一個匿名內部類),隻是發布了一個尚未構造完成的對象。這造成了不正確構造。
常見的使this逸出的操作:
1. 在構造函數中啟動一個線程
如果想在構造函數中注冊一個事件監聽器或啟動線程,可以使用一個私有的構造函數和一個公共的工廠方法
```
class SafeListener {
private final EventListener listener;
private SafeListener {
listener = new EventLIstener() {
public void onEvent(Event e) {doSomething(e);
};
}
static public SafeListener newInstance(EventSource source) {
...//構造、返回
}
3.線程封閉
將數據或對象封閉在一個線程中的技術叫做“線程封閉”。線程封閉將自動實現線程安全性,即使被封閉的對象不是線程安全的。
Java提供了一些機製來幫助實現線程封閉性,如局部變量和ThreadLocal類,但使用時要確保封閉在線程中的對象不會從線程中逸出。
在volatile變量上存在一種特殊的線程封閉,隻要確保隻有單個線程對共享的volatile變量執行寫入操作,那麼就可以安全地在這些共享的volatile變量上執行“讀取--修改--寫入”的操作,在這種情況下,相當於將修改操作封閉在單個線程中以防止發生競態條件,並且volatile變量的可見性保證還確保了其他線程能看到最新的值。
- 棧封閉:隻能通過局部變量訪問對象
- ThreadLocal類:這個類能使線程中的某個值與保存值的對象關聯起來。ThreadLocal類提供了get與set等方法,這些方法為每個使用該變量的線程都存有一份獨立的副本,因此get總是返回當前執行線程在調用set時設置的最新值
4.不變性
滿足同步需求的另一種方法是使用不可變對象。
不可變對象總是線程安全的。
不可變對象不等於將對象中所有的域都聲明為final類型,即使對象中所有的域都是final類型的,這個對象也仍然是可變的,因為在final類型的域中可以保存對可變對象的引用。
不可變對象滿足的條件:
1. 對象創建以後其狀態不能修改
2. 對象的所有域都是final類型(Java中,final除了表示不可變,還表示對象初始化過程是安全的)
3. 對象是正確創建的(創建是this沒有逸出)
5.安全發布
要安全地發布一個對象,對象的引用以及對象的狀態必須同時對其他線程可見。
安全發布的常用模式:
1. 在靜態初始化函數中初始化一個對象引用
2. 將對象的引用保存到volatile類型的域或者AtomicReference對象中
3. 將對象的引用保存到某個正確構造對象的final類型域中
4. 將對象的引用保存到一個由鎖保護的域中
-
通常,要發布一個靜態構造的對象,最簡單和最安全的方式是使用靜態的初始化器:
public static Holder holder = new Holder(42);
靜態初始化器由JVM在類的初始化階段執行,由於JVM內部存在著同步機製,因此通過這種方式初始化的任何對象都可以被安全地發布。
-
事實不可變對象:如果對象從技術上來看是可變的,但其狀態在發布後不會再改變,那麼把這種對象稱為“事實不可變對象(Effectively Immutable Object)”
在沒有額外同步的情況下,任何線程都可以安全地使用被安全發布的事實不可變對象。對象的發布需求取決於它的可變性: 1. 不可變對象可以通過任意機製來發布 2. 事實不可變對象必須通過安全方式來發布 3. 可變對象必須通過安全方式來發布,並且必須是線程安全的或者由某個鎖保護起來
Conclusion
在並發程序中使用和共享對象時,可以使用一些實用策略:
1. 線程封閉。線程封閉的對象隻能由一個線程擁有,對象被封閉在該線程中,並且隻能由這個線程修改
2. 隻讀共享。在沒有額外同步的情況下,共享的隻讀對象可以由多個線程並發訪問,任何線程都不能修改它。共享的隻讀對象包括不可變對象和事實不可變對象
3. 線程安全共享。線程安全的對象在其內部實現同步,因此多個線程可以通過對象的公有接口來進行訪問而不需要進一步的同步
4. 保護對象。被保護的對象隻能通過持有特定的鎖來訪問。保護對象包括封裝在其他線程安全對象中的對象,以及已發布的並且由某個特定鎖保護的對象。
最後更新:2017-11-04 17:33:41