java併發程式設計筆記day1
第三章 共享物件
3.1 可見性
- 在沒有同步的情況下共享變數,可能會導致一直迴圈,並且有可能發生重排序,列印結果為0。
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
3.1.1 過期資料
- 過期資料可能會引發資料的髒讀,錯誤的計算,無限迴圈
public class MutableInteger {
private int value;
public int get() {
return value;
}
public void set(int value ) {
this.value = value;
}
}
在set和get方法中我們都訪問了value,卻沒有進行同步,在多執行緒中可能就導致資料無法及時更新。我們可以同步setter-getter方法來使之成為執行緒安全的。
public class SynchronizedInteger {
@GuardedBy("this") private int value;
public synchronized int get() {
return value;
}
public synchronized void set(int value) {
this.value = value;
}
}
3.1.2 非原子的64位操作
- 最低限的安全值
當一個執行緒沒有同步時,可能會得到一個過期值,但是至少在某個執行緒中我們可以拿到一次我們期望值,而不是憑空產生的值。 - 例外
最底限的安全值應用於所有的變數,除了沒有宣告為volatile的64位數量變值即double和long。JVM允許將64位的讀或寫劃分成兩個32位的操作。這樣就導致讀或寫如果發生在不同執行緒可能會導致一個值高的32位和另一個值底的32位。因此,多執行緒共享變數的double和long,我們要記得將它宣告為volatile,或者用鎖保護起來
3.1.3 鎖和可見性
- 內建鎖
用來確保一個執行緒以某種可預見的方式看見另一個執行緒的影響。如圖所示:
- 鎖不僅僅是關於同步互斥的,也是關於記憶體可見的。為了保證所有的執行緒都能夠看到共享的,可變變數的最新紙,讀取和寫入執行緒必須使用公共的鎖進行同步。
3.1.4 Volatile變數
同步的若形式:volatile
確保一個變數的更新以可預見的形式告知其他的執行緒。編譯期與執行時,會監視這個變數:它是共享的。對它的操作不會與其他的記憶體操作進行重排序。volatile變數不會快取在暫存器或者快取在其他處理器的隱藏位置。對比synchronized,volatile只是輕量級的同步機制,載入volatile變數比載入非volatile變數的開銷略高一點而已。
- 寫入volatile變數就相當於退出同步塊
- 不推薦過度依賴使用volatile變數,比鎖機制更加脆弱,更加難以理解。
- 正確使用volatile方式:1 確保它們所飲用物件的可見性 ; 2用於標識重要的生命週期事件(初始化或關閉)的發生。
- 加鎖能保證原子性和可見性,而volatile只能保證可見性
- 只有滿足以下條件,才去使用volatile變數:
- 1 寫入變數時並不依賴變數的當前值;或者能確保只有單一的執行緒修改變數的值
- 2 變數不需要與其他的狀態變數共同參與不變約束
- 3 訪問變數時,沒有其他的原因需要加鎖
3.2 釋出和溢位
釋出
釋出一個物件是使它能被當前範圍之外的程式碼所使用溢位
一個物件在沒有準備好構造就被髮布出去這種情況稱之為溢位最常見的物件釋出
將物件的引用存入公共靜態域中,釋出一個物件還會間接的釋出其他物件,如下程式碼所示,我們釋出set的時候,同時也將裡面的物件釋出出去了,因為任何程式碼都可以遍歷獲取新Student的引用。類似的我們可以從非私有方法返回一個物件,這也是釋出了這個返回物件:
public class Initialize {
public static Set<Student> studentSets;
public void initailaze(){
knowSecretSets = new HashSet<>();
}
}
- 釋出一個私有陣列(不建議這麼做),這直接將私有的陣列釋出出去了,而這個陣列本應該是私有的。以這種形式釋出的陣列會出問題,因為任何一個呼叫者都可以修改它的內容,事實上已經等同於共有的了。
class UnsafeStates {
private String[] states = new String[]{
"AK", "AL" /*...*/
};
public String[] getStates() {
return states;
}
}
- 釋出一個物件,同時也釋出了該物件裡所有非私有的物件。更可以說,釋出一個物件,那些非私有的引用鏈,和方法呼叫鏈的可獲得物件也都會被髮布。
將一個物件傳遞給外部方法,等同於釋出了這個物件。這就是使用封裝的強制原因:封裝使得程式的正確性分析變得更加可行,而且更不易偶然的破壞涉及約束
釋出內部類,同時也會無條件的釋出封裝這個內部類的封裝類。釋出registerListener時同時將ThisEscape 也釋出出去了,因為內部類的例項包含了對封裝類例項的隱含引用。如下程式碼所示:
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
void doSomething(Event e) {
}
interface EventSource {
void registerListener(EventListener e);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
3.2.1 安全構建的實踐
- 物件只有通過建構函式返回後,才是可預見的,穩定的狀態,所有從建構函式內部發布的物件只是一個未完成構造的物件。即使在建構函式的最後一行釋出的引用也是如此。如果this引用在構造過程中釋出,這樣的物件被認為沒有正確構建的。不要讓this引用在構造中溢位。
不要在建構函式中啟動一個執行緒。
當物件在建構函式中啟動了一個執行緒時,無論是顯示的(通過將他傳給建構函式)還是隱式的(因為Thread或Runnable是所屬物件的內部類),this引用幾乎會被新執行緒共享使用,於是新的執行緒在所屬物件完成構建前就能看見它。在建構函式完成前建立一個執行緒沒有錯,但是不要啟用它,釋出一個start方法來啟動物件擁有的執行緒。在建構函式中呼叫一個可覆蓋的(既不是private和final)例項方法會導致this引用在構造期間溢位。
更明確的說:this引用在建構函式完成前不會從執行緒溢位,只要建構函式完成前沒有其他執行緒使用this引用,this引用就可以通過建構函式儲存到某處
- 如果想在建構函式中註冊監聽器或者啟動執行緒,可以使用一個私有的建構函式和一個公共的工廠方法,這樣能避免不正確的構建。如下程式碼所示:
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
void doSomething(Event e) {
}
interface EventSource {
void registerListener(EventListener e);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
3.3 執行緒封閉
- 不共享資料可以避免同步。執行緒封閉是實現執行緒安全最簡單的方式之一。
- Swing的視覺化元件和資料模型物件並不是執行緒安全的,它們是通過將它們限制到swing的事件分發執行緒中,實現執行緒安全的。
- 一種常用的使用執行緒限制的應用程式就是應用池化的JDBC Connection物件。
執行緒總是從池中獲得一個connection物件,然後用它處理單一的請求,最後歸還給池,每個執行緒都會同步的處理大多請求,而且在connection物件歸還給池之前,池不會再將該connection物件分配給其他執行緒,因此,這種連線管理模式隱式的將connection物件限制在處理請求處理期間的執行緒中。
3.3.1 AD-HOC執行緒限制
指的是維護執行緒限制性的任務全都落在了實現上的這種情況。
不建議使用,用單執行緒化子系統或者棧限制後者thread local取代