3. 對象的共享

1.可見性

指線程之間的可見性。當一個線程修改了對象的某個狀態後,對其他線程是可見的。也就是說其他線程能立即看到這個修改後的結果。

Java指令重排序

在虛擬機層面,爲了儘可能減少內存操作速度遠慢於CPU運行速所帶來的CPU空置的影響。虛擬機會將不滿足happen-before規則的部分代碼的執行順序打亂——即寫在後面的代碼在時間順序上可能先執行,而寫在前面的代碼可能會後執行,以儘可能充分地利用CPU。

happen-before規則

  • 程序次序規則(Program Order Rule):在一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作。準確地說應該是控制流順序而不是代碼順序,因爲要考慮分支、循環等結構。

  • 監視器鎖定規則(Monitor Lock Rule):一個unlock操作先行發生於後面對同一個對象鎖的lock操作。這裏強調的是同一個鎖,而“後面”指的是時間上的先後順序,如發生在其他線程中的lock操作。

  • volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操作先行發生於後面對這個變量的讀操作,這裏的“後面”也指的是時間上的先後順序。

  • 線程啓動規則(Thread Start Rule):Thread獨享的start()方法先行於此線程的每一個動作。

  • 線程終止規則(Thread Termination Rule):線程中的每個操作都先行發生於對此線程的終止檢測,可以通過Thread.join()方法結束、Thread.isAlive()的返回值檢測到線程已經終止執行。

  • 線程中斷規則(Thread Interruption Rule):對線程interrupte()方法的調用優先於被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測線程是否已中斷。

  • 對象終結原則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始。

  • 傳遞性(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。

失效數據

在多線程訪問某個變量時,如果沒有使用同步,很可能獲取到該變量的一個失效值。

下面這個例子用於演示變量的可見性,從理論上可能會輸出0,也有可能不會輸出,但是機率極低。

public class NoVisibility {
    private static boolean ready;
    private static int num;

    public static void main(String[] args) {
        new ReaderThread().start();
        num = 42;
        ready = true;
    }

    static class ReaderThread extends Thread {
        @Override
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(num);
        }
    }
}

失效數據可能會導致輸出錯誤的值,或者使程序無法結束,還有一些令人困惑的bug,比如意料之外的異常、被破壞的數據結構、不精確的計算以及無限循環。

最低安全性(out-of-thin-air-safety)

當線程在沒有同步的情況下讀取變量時,可能會得到一個失效值,但至少這個值是由之前某個線程設置的值,而不是一個隨機值。這種安全性保證被稱爲最低安全性。

非原子的64位操作

Java內存模型要求,變量的讀取操作和寫入操作都必須是原子操作,但是對於非volatile類型的longdouble變量,JVM允許將64位的讀操作和寫操作分解爲兩個32位操作。在多線程程序中使用共享且可變的longdouble變量時考慮使用volatile關鍵字來聲明或者使用鎖進行保護。

使用內置鎖來保證可見性

在共享變量上使用同一個內置鎖來進行讀操作和寫操作的同步,那麼可以保證所有使用該鎖進行同步的線程都能看到共享變量的最新值。

使用volatile關鍵字來保證可見性

當把變量聲明爲volatile類型後,編譯器與運行時都會注意到這個變量是共享的,因此不將該變量上的操作與其他內存操作一起重排序。volatile變量不會緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型變量時總會返回最新寫入的值。

僅當volatile變量能夠簡化代碼的實現以及對同步策略的驗證時,才應該使用它們。如果在驗證正確性時需要對可見性進行復雜的判斷,那麼就不要使用volatile變量。volatile變量的正確使用方式包括:確保它們自身狀態的可見性,確保它們所引用對象的狀態的可見性,以及標識一些重要的程序生命週期事件的發生(例如,初始化或關閉)。

當且僅當滿足以下所有條件時,才應該使用volatile變量:

  • 對變量的寫入操作不依賴變量的當前值,或者能確保只有單個線程更新變量的值。
  • 該變量不會與其他狀態變量一起納入不變性條件中。
  • 在訪問變量時不需要加鎖。

需要注意:

加鎖機制既可以確保可見性又可以確保原子性,而volatile變量只能確保可見性。


2.發佈與逸出

發佈(Publish)對象

使對象能夠在當前作用域之外的代碼中使用。

例如,將一個指向該對象的引用保存到其他代碼可以訪問的地方,或者在某一個非私有的方法中返回該引用,或者將引用傳遞到其他類的方法中。

逸出(Escape)

當某個不應該發佈的對象被髮布時,這種情況被稱爲逸出。

例如,如果在對象構造完成之前就發佈該對象,就會破壞線程安全性。即使沒有線程在去使用這個未完全構造,但是誤用該引用的風險始終存在。

發佈對象的幾種情況

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

    public class Params {
        public static Map<String, Object> params;
    
        public void init() {
            params = new HashMap<String, Object>();
        }
    }
  2. 從非私有方法中返回一個引用。

    public class UnsafeStates {
        private String[] states = new String[] {"AK", "AL"};
    
        public String[] getStates() {
            return states;
        }
    }
  3. 發佈一個內部的類實例,這同時會隱含的發佈外部類的實例。

    public class ThisEscape {
        public ThisEscape(EventSource source) {
            source.registerListener(
                new EventListener() {
                    public void onEvent(Event e) {
                        doSomething(e);
                    }
                }
            );
        }
    }

安全的對象構造過程

不要在構造過程中使this引用逸出。

當且僅當對象的構造函數返回時,對象才處於可預測的和一致的狀態。因此,當從對象的構造函數中發佈對象時,只是發佈了一個尚未構造完成的對象。
在構成過程中使this引用逸出的一個常見錯誤是,在構造函數中啓動一個線程。


3.線程封閉

當訪問共享的可變數據時,通常需要使用同步。一種避免使用同步的方式就是不共享數據。如果僅在單線程內訪問數據,就不要同步。這種技術被稱爲線程封閉(Thread Confinement),它是實現線程安全性的最簡單方式之一。

Ad-hoc 線程封閉

維護線程封閉性的職責完全由程序實現來承擔。

Ad-hoc 線程封閉技術很脆弱,儘量在程序中少使用它。

棧封閉

將局部變量封閉在執行線程的棧中。棧封閉也被稱爲線程內部使用或者線程局部使用。

在Java語言中,保證了基本類型的局部變量始終被封閉在線程中。而引用類型的局部變量則需要小心不要將這些對象錯誤地逸出。

ThreadLocal 類

維持線程封閉性的一種更爲規範的方法是使用ThreadLocalThreadLocal一般稱爲線程本地變量,它是一種特殊的線程綁定機制,將變量與線程綁定在一起,爲每一個線程維護一個獨立的變量副本。通過ThreadLocal可以將對象的可見範圍限制在同一個線程內。
使用ThreadLocal類來爲每個線程獲取一個專屬的SimpleDateFormat對象。

private static final ThreadLocal<DateFormat> DATE_FORMATS = new ThreadLocal<DateFormat>() {
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat();
    }
};

4.不變性

不可變對象(Immutable Object)

如果某個對象在被創建後期狀態不能被修改,那麼這個對象就被稱爲不可變對象。不可變對象一定是線程安全的。

當滿足以下條件時,對象纔是不可變的:

  • 對象創建以後其狀態就不能修改。
  • 對象的所有域都是final類型。
  • 對象都是正確創建的(在對象的創建期間,this引用沒有逸出)。

final 域

final域能夠確保初始化過程的安全性,從而可以不受限制地訪問不可變對象,並在共享這些對象時無須同步。

除非需要某個域是可變的,否則應將其聲明爲final域。


5.安全的發佈對象

不可變對象與初始化安全性

任何線程都可以在不需要額外同步的情況下安全地訪問不可變對象,即使在發佈這些對象時沒有使用同步。

這種保證還將延伸到被正確創建對象中所有final類型的域。在沒有額外同步情況下,也可以安全地訪問final類型的域。但是,如果final類型的域所指向的是可變對象,那麼在訪問這些域所指向的對象的狀態時仍需要同步。

安全發佈的常用模式

要安全地發佈一個對象,對象的引用以及對象的狀態必須同時對其他線程可見,一個正確構造的對象可以通過以下方式來安全地發佈:

  • 在靜態初始化函數中初始化一個對象引用。
  • 將對象的引用保存到volatile類型的域或者AtomicReferance對象中。
  • 將對象的引用保存到某個正確構造對象的final類型域中。
  • 將對象的引用保存到一個由鎖保護的域中。

下面舉例說明:

  • 在靜態初始化函數中初始化一個對象引用。

    public static Number number = new Number(1);

    使用靜態的初始化器是最簡單和最安全的方式。靜態初始化器由JVM在類的初始化階段執行。由於在JVM內部存在着同步機制,因此通過這種方式初始化的任何對象都可以被安全地發佈。

  • 將對象的引用保存到volatile類型的域或者AtomicReferance對象中。

  • 將對象的引用保存到某個正確構造對象的final類型域中

    // 構造一個緩存類,保存計算結果。
    public class OneValueCache {
        private final BigInteger lastNumber;
        private final BigInteger[] lastFactors;
    
        public OneValueCache(BigIneger i, BigInteger[] factors) {
            this.lastNUmber = i;
            this.lastFactors = Arrays.copyOf(factors, factors.length);
        }
    
        public BigInteger[] getFactors(BigInteger i) {
            if (lastNumber == null || !lastNumber.equals(i))
                return null;
    
            return Arrays.copyOf(lastFactors, lastFactors.length);
        }
    }
    
    // 構造一個因式分解的類
    public class Factorizer {
        private volatile OneValueCache cache = new OneValueCache(null, null);
    
        public BigInteger[] calculate(BigInteger i) {
            BigInteger[] factors = cache.getFactors(i);
            if(factors == null) {
                factors = factor(i);
                cache = new OneValueCache(i, factors);
            }
    
            return factors;
        }
    
        private BigInteger[] factor(BigInteger i) {
            // 沒有實現
        }
    }

    cache相關的操作不會相互干擾,因爲OneValueCache是不可變的,並且在每條相應的代碼路徑中只會訪問它一次。通過使用包含多個狀態變量的容器對象來維持不變性,並使用一個volatile類型的引用來確保可見性,Factorizer在沒有顯示使用鎖的情況下仍舊是線程安全的。

  • 將對象的引用保存到一個由鎖保護的域中。
    如果線程A將對象X放入一個線程安全的容器,隨後線程B讀取這個對象,那麼可以確保B看到A設置的X狀態,即便在這段讀/寫X的代碼中沒有包含顯式的同步。
    在Java線程安全庫中的容器類提供了以下的安全發佈保證:

    • 通過將一個鍵或者值放入HashTablesynchronizedMapConcurrentMap中,可以安全地將它發佈給任何從這些容器中訪問它的線程(無論是直接訪問還是通過迭代器訪問)。
    • 通過將某個元素放入VectorCopyOnWriteArrayListCopyOnWriteArraySetsynchronizedListsynchronizedSet中,可以將該元素安全地發佈到任何從這些容器中訪問該元素的線程。
    • 通過將某個元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以將該元素安全地發佈到任何從這些隊列中訪問該元素的線程。

事實不可變對象(Effectively Immutable Object)

如果對象從技術上來看是可變的,但其狀態在發佈後不會再改吧,那麼把這種對象稱爲事實不可變對象。在沒有額外的同步的情況下,任何線程都可以安全地使用被安全發佈的事實不可變對象。

可變對象

如果對象在構造後可以修改,那麼安全發佈只能確保“發佈當時”狀態的可見性。對於可變對象,不僅在發佈對象時需要使用同步,而且在每次對象訪問時同樣需要使用同步來確保後續修改操作的可見性。

對象的發佈需求

對象的發佈需求取決於它的可變性:

  • 不可變對象可以通過任意機制來發布。
  • 事實不可變對象必須通過安全方式來發布。
  • 可變對象必須通過安全方式來發布,並且必須是線程安全的或者由某個鎖保護起來。

安全地共享對象

在併發程序中使用和共享對象時,可以使用一些實用的策略,包括:

  • 線程封閉。線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,並且只能由這個線程修改。
  • 只讀共享。在沒有額外同步的情況下,共享的只讀對象可以由多個線程併發訪問,但任何線程都不能修改他。共享的只讀對象包括不可變對象和事實不可變對象。
  • 線程安全共享。線程安全的對象在其內部實現同步,因此多個線程可以通過對象的公有接口來進行訪問而不需要進一步的同步。
  • 保護對象。被保護的對象只能通過持有特定的鎖來訪問。保護對象包括封裝在其他線程安全對象中的對象,以及已發佈的並且由某個特定鎖保護的對象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章