Java併發編程學習3-可見性和對象發佈

對象的共享

書接上篇,咱們瞭解瞭如何經過同步來避免多個線程在同一時刻訪問相同的數據,而本篇將介紹如何共享和發佈對象,從而使它們可以安全地由多個線程同時訪問。java

1. 可見性

線程安全性的內容,讓咱們知道了同步代碼塊和同步方法能夠確保以原子的方式執行操做。但若是你認爲關鍵字 synchronized 只能用於實現原子性或者肯定「臨界區(Critical Section)」,那就大錯特錯了。同步還有一個重要的方面:內存可見性(Memory Visibility)。咱們不只但願防止某個線程正在使用對象狀態而另外一個線程在同時修改該狀態,並且但願確保當一個線程修改了對象狀態後,其餘線程可以看到發生的狀態變化。編程

可見性是一種複雜的屬性,在通常的單線程環境中,若是向某個變量先寫入值,而後在沒有其餘寫入操做的狀況下讀取這個變量,老是可以獲得相同的值。然而,當讀操做和寫操做在不一樣的線程中執行時,由於沒法確保執行讀操做的線程能適時地看到其餘線程寫入的值,因此也就不能老是獲得相同的值。爲了確保多個線程之間對內存寫入操做的可見性,必須使用同步機制。數組

介紹了這麼多,還不如來看下代碼示例:緩存

/**
 * <p> 在沒有同步的狀況下共享變量(不推薦使用) </p>
 */
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;
    }
}

在上述代碼中,主線程和讀線程都將訪問共享變量 readynumber。主線程啓動讀線程,而後將 number 設爲 42, 並將 ready 設爲 true。讀線程一直循環直到發現 ready 的值變爲 true,而後輸出 number 的值。雖然 NoVisibility 看起來會輸出 42,但事實上極可能輸出0,或者根本沒法終止。由於在代碼中沒有使用足夠的同步機制,因此沒法保證主線程寫入的 ready 值 和 number 值對於讀線程來講是可見的。 安全

若是你嘗試運行該程序,大機率控制檯仍是會輸出42,但這並不說明這塊代碼就老是能輸出想要的結果。NoVisibility 可能會輸出0,這是由於讀線程可能看到了寫入 ready 的值,但卻沒有看到以後寫入 number 的值,這種現象被稱爲 「重排序」;NoVisibility 也可能會一直循環下去,由於讀線程可能永遠都看不到 ready 的值。多線程

在沒有同步的狀況下,編譯器、處理器以及運行時等均可能對操做的執行順序進行一些意想不到的調整。在缺少足夠同步的多線程程序中,要想對內存操做的執行順序進行判斷,幾乎沒法得出正確的結論。併發

1.1 失效數據

NoVisibility 展現了在缺少同步的程序中可能產生錯誤結果的一種狀況:失效數據。當讀線程查看 ready 變量時,可能會獲得一個已經失效的值。更糟糕的是,失效值可能不會同時出現:一個線程可能得到某個變量得最新值,而得到另外一個變量得失效值。ide

下面再看一個代碼示例:函數

/**
 * <p> 非線程安全的可變整數類 </p>
 */
@NotThreadSafe
public class MutableInteger {
    private int value;

    public int getValue() { return value; }
    public void setValue(int value) { this.value = value; }
}

上述代碼中 getset 方法都是在沒有同步的狀況下訪問 value 的。若是某個線程調用了 set 方法,那麼另外一個正在調用 get 方法的線程可能會看到更新後的 value 值,也可能看不到。學習

下面咱們經過對 getset 方法進行同步,可使 MutableInteger 成爲一個線程安全的類。代碼示例以下:

/**
 * <p> 非線程安全的可變整數類 </p>
 */
public class SynchronizedInteger {
    @GuardedBy("this") private int value;

    public synchronized int getValue() { return value; }
    public synchronized void setValue(int value) { this.value = value; }
}

固然若是這裏僅僅對 set 方法進行同步是不夠的,調用 get 方法的線程仍然會看見失效值。

1.2 非原子的64位操做

上面咱們瞭解到,當線程在沒有同步的狀況下讀取變量時,可能會獲得一個失效值,但至少這個值是由以前某個線程設置的值,而不是一個隨機值。這種安全性保證也被稱爲最低安全性(out-of-thin-air-safety)。

最低安全性適用於絕大多數變量,可是非 volatile 類型的64位數值變量例外。Java內存模型要求,變量的讀取操做和寫入操做都必須是原子操做,但對於非 volatile 類型的 longdouble 變量,JVM容許將64位的讀操做或寫操做分解爲兩個32位操做。當讀取一個非 volatile 類型 的 long 變量時,若是對該變量的讀操做和寫操做在不一樣的線程中執行,那麼極可能會讀取到某個值的高32位和另外一個值得低32位。

1.3 加鎖與可見性

內置鎖能夠用於確保某個線程以一種可預測得方式來查看另外一個線程的執行結果,以下圖所示。當線程 A 執行某個同步代碼塊時,線程 B 隨後進入由同一個鎖保護的同步代碼塊,在這種狀況下能夠保證,在鎖被釋放以前,A 看到的變量值在 B 得到鎖後一樣能夠由 B 看到。換句話說,當線程 B 執行由鎖保護的同步代碼塊時,能夠看到線程 A 以前在同一個同步代碼塊中的全部操做結果。
Java併發編程學習3-可見性和對象發佈

加鎖的含義不只僅侷限於互斥行爲,還包括內存可見性。爲了確保全部線程都能看到共享變量的最新值,全部執行讀操做或者寫操做的線程都必須在同一個鎖上同步。

1.4 volatile 變量

Java語言提供了一種稍弱的同步機制,即 volatile 變量,用來確保將變量的更新操做通知到其餘線程。當變量聲明爲 volatile 類型後,編譯器與運行時都會注意到這個變量是共享的,所以不會將該變量上的操做與其餘內存操做一塊兒重排序。volatile 變量不會被緩存在寄存器或者對其餘處理器不可見的地方,所以在讀取 volatile 類型的變量時總會返回最新寫入的值。

固然,這裏不建議過分依賴 volatile 變量提供的可見性。僅當 volatile 變量能簡化代碼的實現以及對同步策略的驗證時,才應該使用它們。若是在驗證正確性時須要對可見性進行復雜的判斷,那麼就建議使用 volatile 變量。

volatile 變量的正確使用方式包括:

  1. 確保它們自身狀態的可見性;
  2. 確保它們所引用對象的狀態的可見性;
  3. 標識一些重要的程序生命週期事件的發生(初始化或關閉)

下面看一個利用 volatile 變量來數綿羊的代碼示例:

volatile boolean asleep;
// ...
    while (!asleep) 
        countSomeSheep();

在如上示例中,線程試圖經過相似數綿羊的傳統方式進入休眠狀態。相比用鎖來確保 asleep 更新操做的可見性,這裏採用 volatile 變量,不只知足了更新操做的可見性,並且代碼邏輯也變得更加簡單,更利於理解。

雖然 volatile 變量使用很方便,但它只能確保可見性,而加鎖機制既能夠確保可見性又能夠確保原子性。

那麼說了這麼多,什麼場景下咱們才應該使用 volatile 變量呢?

當且僅當知足如下條件:

  • 對變量的寫入操做不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
  • 該變量不會與其餘狀態變量一塊兒歸入不變性條件中。
  • 在訪問變量時不須要加鎖。

2. 發佈與逸出

本篇開頭提到了 發佈對象,它是指使對象可以在當前做用域以外的代碼中使用。例如,將一個指向該對象的引用保存到其餘代碼能夠訪問的地方,或者在某一個非私有的方法中返回該引用,或者將引用傳遞到其餘類的方法中。當某個不該該發佈的對象被髮布了,這種狀況就被稱爲 逸出(Escape)。

發佈對象的最簡單方法是將對象的引用保存到一個公有的靜態變量中,以便任何類和線程都可以看見該對象。

下面展現發佈一個對象的代碼示例:

public static Set<Secret> knownSecrets;

    public void initialize() {
        knownSecrets = new HashSet<Secret>();
    }

上述代碼中,在 initialize 方法中示例化一個新的 HashSet 對象,並將對象的引用保存到 knownSecrets 中以發佈該對象。若是將一個 Secret 對象添加到集合 knownSecrets 中,那麼一樣會發布這個 Secret 對象,由於任何代碼均可以遍歷這個集合,並得到對這個新 Secret 對象的引用。

咱們再來看一個代碼示例:

/**
 * <p> 使內部的可變狀態逸出(不推薦使用) </p>
 */
public class UnsafeStates {
    private String[] states = new String[] {"HELLO", "HUAZIE"};

    public String[] getStates() { return states; }
}

上訴代碼從非私有方法 getStates 中返回一個引用,這裏一樣會發布返回的引用的對象 states 。按上述方式來發布 states,就可能存在很大風險,由於任何調用者都能修改這個數組的內容。

若是一個已經發布的對象可以經過非私有的變量引用和方法調用到達其餘的對象,那麼這些對象也都會被髮布。

最後一種發佈對象或其內部狀態的機制就是發佈一個內部的類實例,以下代碼示例:

/**
 * <p> 隱式地使this引用逸出(不推薦使用) </p>
 */
public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(new EventListener(){
            public void onEvent(Event e) {
                doSomething(e);
            }
        });
    }

    private void doSomething(Event e) {
        // 事件處理
    }
}

ThisEscape 發佈 EventListener 時,也隱含地發佈了 ThisEscape 實例自己,由於在這個內部類的實例中包含了對 ThisEscape 實例的隱含引用。

安全的對象構造過程

ThisEscape 中給出了逸出的一個特殊示例,即 this 引用在構造函數中逸出。若是 this 引用在構造過程當中逸出,那麼這種對象就被認爲是不正確構造。

注意: 不要在構造過程當中使 this 引用逸出

若是想在構造函數中註冊一個事件監聽器或啓動進程,那麼可使用一個私有的構造函數和一個公共的工廠方法,從而避免不正確的構造過程。下面請看以下代碼示例:

/**
 * <p> 使用工廠方法來防止this引用在構造過程當中逸出 </p>
 */
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;
    }

    private void doSomething(Event e) {
        // 事件處理
    }
}

結語

本篇咱們一塊兒瞭解了 可見性 和 對象的發佈、逸出等相關內容;關於對象的共享的其餘內容【線程封閉,不變性,安全發佈】,還須要一篇博文才能介紹完,敬請期待!

相關文章
相關標籤/搜索