對象的共享(第三章)

對象的共享

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的語義不足以確保遞增(++)操做的原子性。ide

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

使用volatile變量的時機:函數

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

2.發佈與逸出

  • 發佈(Publish)一個對象:使對象可以在當前做用域以外的代碼中使用。this

    • 當發佈一個對象時,在該對象的非私有域中引用的全部對象一樣會被髮布
    • 當發佈某個對象時,可能會間接發佈其餘對象,如發佈一個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方法)

3.線程封閉

將數據或對象封閉在一個線程中的技術叫作「線程封閉」。線程封閉將自動實現線程安全性,即便被封閉的對象不是線程安全的。 Java提供了一些機制來幫助實現線程封閉性,如局部變量和ThreadLocal類,但使用時要確保封閉在線程中的對象不會從線程中逸出。 在volatile變量上存在一種特殊的線程封閉,只要確保只有單個線程對共享的volatile變量執行寫入操做,那麼就能夠安全地在這些共享的volatile變量上執行「讀取--修改--寫入」的操做,在這種狀況下,至關於將修改操做封閉在單個線程中以防止發生競態條件,而且volatile變量的可見性保證還確保了其餘線程能看到最新的值。code

  • 棧封閉:只能經過局部變量訪問對象
  • 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. 保護對象。被保護的對象只能經過持有特定的鎖來訪問。保護對象包括封裝在其餘線程安全對象中的對象,以及已發佈的而且由某個特定鎖保護的對象。
相關文章
相關標籤/搜索