Java併發編程學習4-線程封閉和安全發佈

對象的共享

3. 線程封閉

線程封閉(Thread Confinement)是實現線程安全性的最簡單方式之一。當某個對象封閉在一個線程中時,這種用法將自動實現線程安全性,即便被封閉的對象自己不是線程安全的。html

在Java中使用線程封閉技術有:Swing 和 JDBC 的 Connection 對象。java

  • Swing 的可視化組件和數據模型對象都不是線程安全的,Swing 經過將它們封閉到 Swing 的事件分發線程中來實現線程安全性;爲了進一步簡化對 Swing 的使用,Swing 還提供了 invokeLater 機制,用於將一個 Runnable 實例調度到事件線程中執行。
  • 在典型的服務器應用程序中,線程從鏈接池中得到一個 Connection 對象,而且用該對象來處理請求,使用完後再將對象返還給鏈接池。在這個過程當中,大多數請求(例如 Servlet 請求 或 EJB 調用)都是由單個線程採用同步的方式來處理,而且在 Connection 對象返回以前,鏈接池不會再將它分配給其餘線程。也就是說,這種鏈接管理模式在處理請求時隱含地將 Connection 對象封閉在線程中。

3.1 Ad-hoc 線程封閉

Ad-hoc 線程封閉是指,維護線程封閉性的職責徹底由程序實現來承擔。由於沒有任何一種語言特性,能將對象封閉到目標線程上,因此 Ad-hoc 線程封閉是很是脆弱的。而正因爲 Ad-hoc 線程封閉技術的脆弱性,在程序中咱們應儘可能少用它,在可能的狀況下,應該使用更強的線程封閉技術(例以下面要介紹的 棧封閉 或 ThreadLocal 類)。git

3.2 棧封閉

棧封閉是線程封閉的一種特例(它也被稱爲線程內部使用或線程局部使用),在棧封閉中,只能經過局部變量才能訪問對象。由於局部變量的固有屬性之一就是封閉在執行線程中,它們位於執行線程的棧中,其餘線程沒法訪問這個棧。所以棧封閉比 Ad-hoc 線程封閉更易於維護,也更加健壯。github

3.3 ThreadLocal 類

ThreadLocal 對象一般用於防止對可變的單實例變量或全局變量進行共享。它提供了 getset 等訪問方法,這些方法爲每一個使用該變量的線程都存有一份獨立的副本,所以 get 老是返回由當前執行線程在調用 set 時設置的最新值。緩存

下面一塊兒來看下面的代碼示例:安全

private static ThreadLocal<Connection> connectionHolder = 
        new ThreadLocal<Connection>() {
            public Connection initialValue() {
                return DriverManager.getConnection(DB_URL);
            }
        };

    public static Connection getConnection() {
        return connectionHolder.get();
    }

上述代碼經過將 JDBC 的鏈接保存到 ThreadLocal 對象中,每一個線程都會擁有屬於本身的鏈接。當某個線程初次調用 getConnection 方法時,就會調用 ThreadLocalinitialValue 來獲取初始化的鏈接對象。服務器

那麼該怎麼理解 ThreadLocal\<T> 對象呢 ?從概念上看,能夠將 ThreadLocal\<T> 視爲包含了 Map\<Thread, T> 對象,其中保存了特定於該線程的值。固然 ThreadLocal 的實現並不是如此。這些特定於線程的值保存在 Thread 對象中,當線程終止後,這些值會做爲垃圾回收。多線程

值得注意的是,ThreadLocal 變量相似於全局變量,它可能會下降代碼的可重用性,並在類之間引入隱含的耦合性,所以在使用時要格外當心。併發

4. 不變性

到目前爲止,咱們介紹了許多與原子性和可見性相關的問題,例如獲得失效的數據,丟失更新操做或者觀察到某個對象處於不一致的狀態等等,都與多線程試圖同時訪問同一個可變的狀態相關。若是對象的狀態不會改變,那麼這些問題天然也就迎刃而解。ide

若是某個對象在被建立後其狀態就不能被修改,那麼咱們就能夠稱它爲不可變對象。線程安全性是不可變對象的固有屬性之一,它的不變性條件是由構造函數建立的,只要它的狀態不改變,那麼這些不變性條件就能一直維持下去。

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

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

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

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

在不可變對象的內部仍可使用可變對象來管理它們的狀態。

下面咱們來看以下的代碼示例:

/**
 * <p> 在可變對象基礎上構建的不可變類 </p>
 */
@Immutable
public final class ThreeStooges {
    private final Set<String> stooges = new HashSet<>();

    public ThreeStooges() {
        stooges.add("Tom");
        stooges.add("Jerry");
        stooges.add("Huazie");
    }

    public boolean isStooge(String name) {
        return stooges.contains(name);
    }
}

上述代碼中 ThreeStooges 能夠稱爲不可變對象。能夠從以下三個方面來理解:

  • 儘管保存臭皮匠姓名的 Set 對象是可變的,但從代碼的設計上能夠看到,在 Set 對象構造完成後沒法對其進行修改。
  • stooges 是一個 final 類型的引用變量,所以全部的對象狀態都經過的一個 final 域來訪問。
  • ThreeStooges 的構造函數中無 this 引用的逸出,能夠正確地構造對象。

4.1 Final 域

關鍵字 final 用於構造不可變的對象。final 類型的域是不能修改的,但若是 final 域所引用的對象是可變的,那麼這些引用的對象是能夠修改的。

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

4.2 不可變對象的簡單示例

在以前的博文中,咱們介紹了 UnsafeCachingFactorizer,嘗試用兩個 AtomicReferences 變量來保存最新的數值及其因數分解結果,但這種方式並不是是線程安全的,由於咱們沒法以原子方式來同時讀取或更新這兩個相關的值。

下面咱們介紹一種 使用 volatile 類型來發佈一個不可變對象 的方案:

(1)首先,咱們來看一個不可變的類 OneValueCache ,用於存儲最新的數值及其因數分解的結果。

/**
 * <p> 對數值及其因數分解結果進行緩存的不可變容器類 </p>
 */
@Immutable
public class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger lastNumber, BigInteger[] lastFactors) {
        this.lastNumber = lastNumber;
        if (null != lastFactors) {
            this.lastFactors = Arrays.copyOf(lastFactors, lastFactors.length);
        } else {
            this.lastFactors = null;
        }
    }

    public BigInteger[] getFactors(BigInteger i) {
        if (null == lastNumber || !lastNumber.equals(i))
            return null;
        else
            return Arrays.copyOf(lastFactors, lastFactors.length);
    }
}

對於在訪問和更新多個相關變量時出現的的競態條件問題,能夠經過將這些變量所有保存在一個不可變對象中來消除。若是要更新這些變量,那麼能夠建立一個新的容器對象,而其餘使用原有對象的線程仍然會看到對象處於一致的狀態。

注意: 若是在 OneValueCachegetFactors 方法和構造函數中,沒有調用 Arrays.copyOf , 那麼 OneValueCache 就不是不可變的。

(2)而後,咱們來看使用了修飾爲 volatile 類型的 OneValueCache 的因數分解實現。

/**
 * <p> 使用執行不可變容器對象的 volatile 類型引用以緩存最新的結果 </p>
 */
public class VolatileCachedFactorizer extends HttpServlet {
    private volatile OneValueCache cache = new OneValueCache(null, null);

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        BigInteger i = CommonUtils.extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);
        if (null == factors) {
            factors = Factor.factor(i);
            cache = new OneValueCache(i, factors);
        }
        CommonUtils.encodeIntoResponse(resp, factors);
    }
}

(3)最後,咱們簡單分析下上述代碼。由於 OneValueCache 是不可變的,而且在每條相應的代碼路徑中只會訪問它一次,因此與 cache 變量相關的操做不會互相干擾,也就保證了因數分解過程的線程安全。經過使用包含多個狀態變量的容器對象來維持不變性條件,並使用一個 volatile 類型的引用來確保可見性,使得 VolatileCachedFactorizer 在沒有顯式地使用鎖的狀況下仍然是線程安全的。

5. 安全發佈

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

下面咱們先看一個發佈對象的簡單示例:

// 在沒有足夠同步的狀況下發布對象
    public Holder holder;

    public void initialize() {
        holder = new Holder(42);
    }

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

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

下面咱們直接來看以下代碼示例:

/**
 * <p> 因爲未被正確發佈,所以這個類在調用 assertSanity時將拋出 AssertionError </p>
 */
public class Holder {
    private int n;

    public Holder(int n) {
        this.n = n;
    }

    public void assertSanity() {
        if (n != n) {
            throw new AssertionError("This statement is false.");
        }
    }
}

上述代碼中因爲沒有使用同步來確保 Holder 對其餘線程可見,所以將 Holder 稱爲 「未被正確發佈」。

在未被正確發佈的對象中存在兩個問題:

  • 除了發佈對象的線程外,其餘線程能夠看到的 Holder 域是一個失效值,所以將看到一個空引用或者以前的舊值。
  • 發佈對象的線程看到 Holder 引用的值是最新的,但 Holder 狀態的值倒是失效的。某個線程在第一次讀取域時獲得失效值,而再次讀取這個域時會獲得一個更新值,這也是 Holder 類調用 assertSanity 拋出 AssertionError 的緣由。

注意: 儘管在構造函數中設置的域值彷佛是第一次向這些域中寫入的值,所以不會有 「更舊的」 值被視爲失效值,但 Object 的構造函數會在子類構造函數運行以前先將默認值寫入全部的域。所以,某個域的默認值可能被視爲失效值。

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

Java內存模型爲不可變對象的共享提供了一種特殊的初始化安全性保證。即便在發佈不可變對象的引用時沒有使用同步,也仍然能夠安全地訪問該對象。

這種安全性保證還將延伸到被正確建立對象中全部 final 類型的域。在沒有額外同步的狀況下,也能夠安全地訪問 final 類型的域。可是若是 final 類型的域所指向的是可變對象,那麼在訪問這些域所指向的對象的狀態時仍然須要同步。

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

要安全地發佈一個對象,對象的引用以及對象的狀態必須同時對其餘線程可見。

能夠經過如下方式來安全的發佈一個正確構造的對象:

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

在線程安全容器內部的同步意味着,在將對象放入到某個容器,將知足上述最後一條方式。若是線程 A 將對象 X 放入一個線程安全的容器,隨後線程 B 讀取這個對象,那麼能夠確保 B 看到 A 設置的 X 狀態,即使這段讀/寫 X 的應用程序代碼沒有包含顯式的同步。

Java的線程安全庫中的容器類有不少,下面列舉一些它們提供的安全發佈保證:

  • 經過將一個鍵或者值放入 HashtableCollections.synchronizedMap 或者 ConcurrentMap 中,能夠安全地將它發佈給任何從這些容器中訪問它的線程(不管是直接訪問仍是經過迭代器訪問)。
  • 經過將某個元素放入 VectorCopyOnWriteArrayListCopyOnWriteArraySetCollections.synchronizedListCollections.synchronizedSet 中,能夠將該元素安全地發佈到任何從這些容器中訪問該元素的線程。
  • 經過將某個元素放入 BlockingQueue 或者 ConcurrentLinkedQueue 中,能夠將該元素安全地發佈到任何從這些隊列中訪問該元素的線程。
  • 類庫中的其餘數據傳遞機制(例如 Future 和 Exchanger)一樣能實現安全發佈,這些後續介紹這些機制將會仔細討論。

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

public static Holder holder = new Holder(42);

靜態初始化器由 JVM 在類的初始化階段執行。因爲在 JVM 內部存在着同步機制,所以經過這種方式初始化的任何對象均可以被安全地發佈。

5.4 事實不可變對象

若是對象從技術上來看是可變的,但其狀態在發佈後不會再改變,那麼這種對象也稱爲 「事實不可變對象【Effectively Immutable Object】」。

全部的安全發佈機制都能確保,當對象的引用對全部訪問該對象的線程可見時,對象發佈時的狀態對於全部線程也將是可見的,而且若是該對象狀態不會再改變,那麼就足以確保任何訪問都是安全的。

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

下面咱們來看一個代碼示例:

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

上述代碼假設須要維護一個保存了每位用戶的最近登陸時間的 Map。若是 Date 對象的值在被放入 Map 後就不會改變,那麼 synchronizedMap 中的同步機制就足以使 Date 值被安全地發佈,而且在訪問這些 Date 值時不須要額外的同步。

5.5 可變對象

若是對象在構造後能夠修改,那麼安全發佈只能確保 「發佈當時」 狀態的可見性。對於可變對象不只在發佈對象時須要使用同步,並且在每次對象訪問時一樣須要使用同步來確保後續修改操做的可見性。

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

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

5.6 安全地共享對象

在併發程序中使用和共享對象時,可使用以下一些實用的方法:

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

結語

對象的共享 到這裏就介紹完畢了,下一篇咱們將開始瞭解 對象的組合,敬請期待!!!

相關文章
相關標籤/搜索