1. 程式人生 > >java併發程式設計筆記day1

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取代