【Java併發.3】對象的共享

  本章將介紹如何共享和發佈對象,從而使他們可以安全地由多個線程同時訪問。這兩章合在一塊兒就造成了構建線程安全類以及經過 java.util.concurrent 類庫來構建開發併發應用程序的重要基礎。html

3.1  可見性java

  可見性是一種複雜的屬性,由於可見性中的錯誤老是違背咱們的直覺。爲了確保多個線程之間對內存寫入操做的可見性,必須使用同步機制數據庫

  在下面的清單中 NoVisibility 說明了當多個線程在沒有同步的狀況下共享數據出現的錯誤。主線程啓動讀線程,而後將 number 設爲 42,並將 ready 設爲 true。讀線程一直循環直到發現 ready 的值變爲 true,而後輸出 number 的值。雖然看起來會輸出 42,但事實上可能輸出 0,或者根本沒法終止。這是由於代碼中沒有使用足夠的同步機制,所以沒法保證主線程寫入的ready 值和 nunber 值對於讀線程來講是可見的。編程

public class NoVisibility {                    【皺眉臉-不要這樣作private static boolean ready;
    private static int number;

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready) {
                Thread.yield();
            }
            System.out.println(number);
        }
    }
}

  NoVisibility 可能會持續循環下去,由於讀線程可能永遠都看不到 ready 值。一種更奇怪的現象是,NoVisibility 可能會輸出 0,由於讀線程可能看到了寫入 ready 值,但卻沒有看到以前寫入 number 值,這種現象稱爲「重排序(Reordering)」。(註釋:這看上去彷佛是一種失敗的設計,但倒是使 JVM 充分地利用現代多核處理器的強大性能。)數組

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

 

3.1.1  失效數據安全

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

  失效數據還可能致使一些使人困惑的故障,例如意料以外的異常、被破壞的數據結構、不精確的計算以及無限循環等。多線程

  在以下程序清單 Mutableinteger 不是線程安全的,由於 get 和 set 都是沒有同步的狀況下訪問 value 的。若是某個線程調用了 set,那麼另外一個在調用的get 線程可能會看到更新後的值,也可能看不到。併發

public class MutableInteger {
    private int value;
    public int get() {
        return value;
    }
    public void set(int value) {
        this.value = value;
    }
}

  在程序清代 SynchronizedInteger 中,經過對 get 和 set 方法進行同步,可使MutableInteger 成爲一個線程安全的類。僅對 set 方法進行同步時不夠的,調用 get 線程仍然會看到失效值。ide

public class SynchronizedInteger  {
    private int value;
    public synchronized int get() {
        return value;
    }
    public synchronized void set(int value) {
        this.value = value;
    }
}

 

3.1.2  非原子的64位操做

  忽略。。。

 

3.1.3  加鎖與可見性

  內置鎖能夠用於確保某個線程以一種可預測的方式來查看另外一個線程的執行結果。對於同一個鎖,後面進入鎖的線程能夠看到以前線程在鎖中的全部操做結果(加鎖能夠保證可見性)。

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

 

3.1.4  Volatile變量

  對於volatile 關鍵字的詳細介紹,建議你們去仔細觀看 volatile關鍵字解析 ,因此在這不作介紹。

 

3.2  發佈與逸出

  「發佈(Publish)」一個對象的一塊兒是指,是對象可以在當前做用域以外的代碼中使用。例如,將一個指向該對象的引用保存到其餘代碼能夠訪問的地方,或者在某一個非私有的方法中返回該引用,或者將引用傳遞到其餘類的方法中。在許多狀況中,咱們要確保對象及其內部狀態不被髮布。而在某些狀況下,咱們又須要發佈某個對象,但若是在發佈時要確保線程安全性,則可能須要同步。當某個不該該發佈的對象被髮布時,這種狀況就被稱爲逸出(Escape)

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

public class KnownSecrets {
    public static Set<Secret> knownSecrets;
    public void initialize() {
        knownSecrets = new HashSet<Secret>();
    }
}

  程序清單:是內部的可變狀態逸出:

public class UnsafeStates {
    private String[] states = new String[] {"AK","AL"...};
    public String[] getStates() {
        return states;
    }
}

  如何按照上述方式來發布 states,就會出現問題,由於任何調用者都能修改這個數組的內容。在這個實例中,數組 states 已經逸出了它所在的做用域,由於這個本應是私有的變量已經被髮布了。

  當發佈一個對象時,在該對象的非私有域中引用的全部對象一樣會被髮布。通常來講,若是一個已經發布的對象可以經過非私有的變量引用和方法調用到達其餘的對象,那麼這些對象也都會被髮布。

 

3.3  線程封閉

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

  線程封閉技術的常見應用時 JDBC 的 Connection 對象。線程從鏈接池中得到一個 Connection 對象,而且用該對象來處理請求,使用完後再將對象返還給鏈接池。因爲大多數請求都是由單個線程採用同步的方式來處理,而且在 Connection 對象返回以前,鏈接池不會再將它分配給其餘線程,所以,這種鏈接管理模式在處理請求時隱含地將 Connection 對象封閉在線程中。

 

3.3.1  Ad-hoc線程封閉

  略...

 

3.3.2  棧封閉

  棧封閉式線程封閉的一種特例,在棧封閉中,只能經過局部變量才能訪問對象。局部變量的固有屬性之一就是封閉在執行線程中。它們位於執行線程的棧中,其餘線程沒法訪問這個棧。棧封閉(也被稱爲線程內部使用或者線程局部使用,不要與核心類庫中的 ThreadLocal 混淆)。

  對於基本類型的局部變量,以下程序清單中 loadTheArk 方法的 numPairs,不管如何都不會破壞棧封閉性,因爲任何方法都沒法得到基本類型的引用,所以Java 語言的這種語義就確保了基原本興的局部變量始終封閉在線程內。

public int loadTheArk(Collection<Animal> candidates) {
        SortedSet<Aniaml> animals;
        int numPairs = 0;   //基本類型的局部變量
        Aniaml candidate = null;
        // animals 被封閉在方法中,不要使它們逸出
        animals = new TreeSet<Animal>(new SpeciesGenderComparator());
        animals.addAll(candidates);
        for (Animal a : animals) {
            numPairs++;
        }
        return numPairs;
}

 

3.3.3  ThreadLocal 類

  維持線程封閉性的一種更規範方法就是使用 ThreadLocal,這個類能使線程中的某個值與保存值的對象關聯起來。ThreadLocal 提供了 get 和 set 等訪問接口或方法,這些方法爲每一個使用該變量的線程都存有一份獨立的副本,所以 get 老是返回由當前執行線程在調用 set 時設置的最新值

  ThreadLocal 對象一般用於放置對可變的單實例變量(Singleton)或全局變量進行共享。例如,在單線程應用程序中可能會維持一個全局的數據庫鏈接,並在程序啓動時初始化這個鏈接對象,從而避免在調用每一個方法時都要傳遞一個 Connection 對象。

  private static ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<>() {
        @Override
        protected Object initialValue() {
            return DriverManager.getConnection(URL);
        }
    }
    
    public static Connection getConnection() {
        return connectionThreadLocal.get();
    }

  當某個線程初次調用 ThreadLocal.get 方法時,就會調用 initialValue 來獲取初始值。從概念上看,你能夠將 ThreadLocal<T> 視爲包含了 Map<Thread, T> 對象,其中保存了特定於該線程的值,但 ThreadLocal 的實現並不是如此。這些特定於線程的值保存在 Thread 對象,當線程終止後,這些值會做爲垃圾回收

 

3.4  不變性

  知足同步需求的另外一種方法時使用不可變對象。到目前爲止,咱們介紹了許多與原子性和可見性相關的問題,例如獲得失效數據,丟失更新操做或者觀察到某個對象處於不一致的狀態等等,都與多線程試圖同時訪問同一個可變的狀態相關。若是對象的狀態不會改變,那麼這些問題與複雜性也就天然消失了。

不可變對象必定是線程安全的。

  雖然在Java 語言規範和 Java 內存模型中都沒有給出不可變性的正式定義,但不可變性並不等於將對象中全部的域都聲明爲 final 類型,即便對象中全部的域都是 final 類型的,這個對象也仍然是可變的,由於在 final 類型的域中能夠保存對可變對象的引用。

當知足如下條件時,對象纔是不可變的:
  • 對象建立之後其狀態不可能修改。
  • 對象的全部域都是 final 類型。
  • 對象時正確建立的(在對象的建立期間, this 引用沒有逸出)。

  看個例子:在可變對象基礎上構建的不可變類

public class ThreeStooges {
    private final Set<String> stooges = new HashSet<>();
    public ThreeStooges() {
        stooges.add("one");
        stooges.add("two");
        stooges.add("three");
    }
    public boolean isStooge(String name) {
        return stooges.contains(name);
    }
}

 

3.4.1  Final 域

  在 Java 內存模型中,final 域還有着特殊的語義。final 域能確保初始化過程的安全性,從而能夠不受限制地訪問不可變對象,並在共享這些對象時無需同步。

正如「除非須要更高的可見性,不然應將全部的域都聲明爲私有域」是一個良好的編程習慣,「除非須要某個域是可變的,不然應將其聲明爲 final 域」也是一個良好的編程習慣。

 

3.4.2  示例:使用 volatile 類型來發布不可變對象

  對於volatile 關鍵字的詳細介紹,建議你們去仔細觀看 volatile關鍵字解析 ,因此在這不作過多介紹。貼一個代碼:

public class VolatileCachedFactorizer implements Servlet {
    private volatile OneValueCache cache = new OneValueCache(null, null);
    public void service(ServletRequest request, ServletResponse response) {
        BigInteger i = extractFromRequest(request);
        BigInteger[] factors = cache.getFactors(i);
        if (factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse(response, factors);
    }
}

 

3.5  安全發佈

  到目前爲止,我麼重點討論的是如何確保對象不被髮布,例如讓對象封閉在線程或另外一個對象的內部。固然,在某些狀況下咱們但願多個線程間共享對象,此時必須確保安全地進行共享。

  以下:在沒有足夠同步的狀況下發布對象(不要這樣作)

//不安全的發佈
public Holder holder;
public void initialize() {
    holder = new Holder(42);
}

  因爲可見性問題,其餘線程看到的 Holder 對象將處於不一致的狀態,即使在該對象的構建函數中已經正確地構建了不便性條件。這種不正確的發佈致使其餘線程看到還沒有建立完成的對象。

 

3.5.1  不正確的發佈:正確的對象被破壞

  你不能期望一個還沒有被徹底建立的對象擁有完整性。某個觀察該對象的線程將看到對象處於不一致的狀態,而後看到對象的狀態忽然發生變化,即便線程在對象發佈後尚未修改過它。

  以下:因爲未被正確發佈,所以這個類可能出現故障

public class Holder {
    private int n;
    public Holder(int n) {
        this.n = n;
    }
    public void assertSanity() {
        if(n != n)    //這句沒看懂,就算同步時會出現 n 極可能成爲失效值,可是難道 (n != n)不是原子操做?求解。
        throw new AssertionError("this statement is false");
    }
}

 

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

   因爲不可變對象是一種很是重要的對象,所以Java 內存模型爲不可變對象的共享提供了一種特殊的初始化安全性保障。

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

 

3.5.3  安全發佈的經常使用模式

    要安全地發佈一個對象,對象的引用以及對象的狀態必須同時對其餘線程可見。一個正確構造的對象能夠經過如下方式來安全地發佈:
  • 在靜態初始化函數中初始化一個對象引用。
  • 將對象的引用保存到 volatile 類型的域或者 AtomicReferance 對象中
  • 將對象的引用保存到某個正確構造對象的 final 類型域中。
  • 將對象的引用保存到一個由鎖保護的域中。

  線程安全庫中的容器類提供了一下的安全發佈保證:

  • 經過將一個鍵或者值放入 Hashtable、synchronizedMap 或者 ConcurrentMap 中,能夠安全地將它發佈給任何從這些同期中訪問它的線程(不管是直接訪問仍是經過迭代器訪問)
  • 經過將某個元素放入 Vector、CopyiOnWriteArrayList、CopyOnWriteArraySet、synchronizedListsynchronizedSet 中,能夠將該元素安全地發佈到任何從這些容器中訪問該元素的線程。
  • 經過將某個元素放入 BlockingQueue 或者 ConcurrentLinkedQueue 中,能夠將該元素安全地發佈到任何從這些隊列中訪問該元素的線程。

  一般,要發佈一個靜態構造的對象,最簡單和最安全的方式是使用靜態的初始化器:

public static Holder holder = new Holder(42);

 

3.5.4  事實不可變對象

  若是對象在發佈後不會被修改,那麼 程序只需將它們視爲不可變對象便可。

在沒有額外的同步狀況下,任何線程均可以安全地使用被安全發佈的事實不可變對象。

  例如,Date 自己是可變的,但若是將它做爲不可變對象來使用,那麼在多個線程之間共享 Date 對象時,就能夠省去對鎖的使用。假設須要維護一個 Map 對象,其中保存了每位用戶的最近登陸時間:

public Map<String, Date> lastLogin = Collections.synchronizedMap(new HashMap<String, Date>());

  若是Date對象的值在被放入Map 後就不會改變,那麼 synchronizedMap 中的同步機制就足以使 Date 值被安全地發佈,而且在訪問這些 Date 值時不須要額外的同步。

 

3.5.5  可變對象

  對於可變對象,不只在發佈對象時須要使用同步,並且在每次對象訪問時一樣須要使用同步來確保後續修改操做的可見性。

    對象的發佈須要取決於它的可變性:
  • 不可變對象能夠經過任何機制來發布
  • 事實不可變對象必須經過安全方式來發布。
  • 可變對象必須經過安全方式來發布,而且必須是線程安全的或者由某個鎖保護起來。

 

3.4.5  安全地共享對象

  當發佈一個對象時,必須明確地說明對象的訪問方式。

    在併發程序中使用和共享對象時,可使用一些實用的策略包括:
  線程封閉:線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,而且只能由這個線程修改。
  只讀共享:在沒有額外同步的狀況下,共享的只讀對象能夠由多個線程併發訪問,但任何線程都不能修改它。共享的只讀對象包括不可變對象和事實不可變對象。
  線程安全共享:線程安全的對象在其內部實現同步,所以對個線程能夠經過對象的公有接口來進行訪問而不須要進一步的同步。
  保護對象:被保護的對象只能經過持有特定的鎖來訪問。保護對象包括封裝在其餘線程安全對象中的對象,以及已發佈的而且由某個特定鎖保護的對象。
相關文章
相關標籤/搜索