第3章 對象的共享

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

使用:

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

 

volatile變量的一種典型用法:檢查某個專題標記以判斷是否退出循環,eg:

volatile boolean asleep;

...

while(!asleep)

    countSomeSheep();

...

 
volatile變量通常用作某個操作完成,發生終端或者狀態的標誌。儘管volatile變量也可以用於表示其他的狀態信息,但是volatile語義不足以確保遞增操作(count++)的原子性,除非你能確保只又一個線程對變量執行寫操作。

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

1. 對變量的寫入操作不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。

2. 該變量不會與其他狀態變量一起納入不變性條件中。

3. 在訪問變量時不需要加鎖

 
 
 
線程封閉

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

它是實現線程安全的最簡單方式之一。

 
線程封閉技術常見的例子就是JDBC的Connection對象,JDBC規範並不要求Connection對象必須是線程安全的。在典型的服務器應用程序中,線程從連接池中獲得一個Connection對象,並且用該對象來處理請求,使用完後再將對象返還給連接池。由於大多數請求(例如Servlet請求或EJB調用多等)都是由單個線程採用同步的方式來處理,並且在Connection對象返回之前,連接池不會再將它分配給其他線程。因此,這種連接管理模式再處理請求時隱含的將Connection對象封閉在線程中。
Java語言及其核心庫提供了一些機制來幫助維持線程封閉性,例如局部變量和ThreadLocal類,但即便如此,程序員仍然需要負責確保封閉在線程中的對象不會從線程中逸出。

Ad-hoc線程封閉

Ad-hoc線程封閉是指,維護線程封閉性的職責完全由程序來承擔。Ad-hoc線程封閉是非常脆弱的,因此在程序中儘量少用,在可能的情況下,應該使用更強的線程封閉技術(例如,棧封閉 或 ThreadLocal類)。

 

棧封閉
棧封閉是線程封閉的一種特例,在棧封閉中,只能通過局部變量才能訪問對象。局部變量的固有屬性之一就是封閉在執行線程中。它們位於執行線程的棧中,其他線程無法訪問這個棧。棧封閉(也被稱爲線程內部使用或者線程局部使用,不要與核心類庫中的ThreadLocal混淆)比Ad-hoc線程封閉更易於維護,也更加健壯。
 
ThreadLocal類
維持線程封閉性的一種更規範的方法是使用ThreadLocal,這個類能使線程中的某個值與保存值的對象關聯起來。ThreadLocal提供了get與set等訪問接口或方法,這些方法爲每個使用該變量的線程都存有一份獨立的副本,因此get總是返回由當前執行線程在調用set時設置的最新值。
ThreadLocal對象通常用於防止對可變的單實例變量(Single)或全局變量進行共享。例如,在單線程應用程序中可能會維持一個全局的數據庫連接,並在程序啓動時初始化這個連接對象,從而避免在調用每個方法時都要傳遞一個Connection對象。由於JDBC的連接對象不一定是線程安全的,因此,當多線程應用程序在沒有協同的情況下使用全局變量時,就不是線程安全的。通過將JDBC的連接保存到ThreadLocal對象中,每個線程都會擁有屬於自己的連接。
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<connection>(){
        public Connection initialValue() {
            return DriverManager.getConnection(DB_URL);
        }
    };
public static Connection getConnection(){
    return connectionHolder.get();
}
 
ThreadLocal變量類似於全局變量,它能降低代碼的可重用行,並在類之間引入隱含的耦合性,因此在使用時要格外小心。
3.4 不變性
滿足同步的另一種方法是使用不可變對象(Immutable Object)。不可變對象一定是線程安全的。
雖然Java語言規範和Java內存模型中都沒有給出不可變性的正式定義,但不可變性並不等於將對象中所有的域都聲明爲final類型,即使對象中所有的域都是final類型的,這個對象也仍然是可變的,因爲在final類型的域中可以保存對可變對象的引用。

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

1.對象創建以後其狀態就不能修改

2.對象的所有域都是final類型

3.對象是正確創建的(在對象的創建期間,this引用沒有逸出)

 
使用volatile類型來發布不可變對象
@Immutable
class OneValueCache{
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger i,BigInteger[] factors){
        lastNumber = i;
        lastFactors = factors;
    }

    public BigInteger[] getFactors(BigInteger i){
        if(lastNumber==null || !lastNumbers.equal(i)) return null;
        else return Arrays.copyOf(lastFactors,lastFactors.length);
    }
}
 對於在訪問和更新多個相關變量時出現的競爭條件問題,可以通過將這些變量全部存在一個不可變的對象中來消除。如果是一個可變的對象,那麼就必須使用鎖來確保原子性。如果是一個不可變對象,那麼當現場獲得了對該對象的引用後,就不必擔心另一個線程會修改該對象的狀態。如果要更新這些變量,那麼可以創建一個新的容易對象,但其他使用原有對象的線程仍然會看到對象處於一致的狀態。
@ThreadSafe
public class VolatileCachedFactorizer implements Servlet{
    private volatile OneValueCache cache = new OneValueCache(null,null);
    public void service(ServletRequest req,ServletResponse resp){
          BigInteger i = extractFromRequest(req);
          BigInteger[] factors = cache.getFactors(i);
          if(factors==null){
                 factors = factor(i);
                 cache = new OneValueCache(i,factors);
          }
          encodeIntoResponse(resp,factors);
     }
}
與cache 相關的操作不會相互干擾,因爲OneValueCache是不可變的,並且在每條相應的代碼路徑中只會訪問它一次。通過使用包含多個狀態變量的容器對象來維持不變性條件,並使用一個volatile類型的引用確保可見性,使得在沒有使用鎖的情況下仍然是線程安全的。
3.5.2 不可變對象與初始化安全性
Java內存模型爲不可變對象的共享提供了一種特殊的初始化安全保證。我們已經知道,即使某個對象的引用對其他線程是可見的,也並不意味着對象狀態對於該對象的線程來說一定是可見的。爲了確保對象狀態能呈現出一隻的視圖,就必須使用同步。
另一方面,即使在發佈不可變對象的引用時沒有使用同步,也仍然可以安全地訪問該對象,爲了維持這種初始化安全性的保證,必須滿足不可變性的所有需求:狀態不可修改,所有域都是final類型,以及正確的構造過程。
這種保證還將延伸到被正確創建對象中所有final類型的域。在沒有額外同步的情況下,也可以安全地訪問final類型的域。然而,如果final類型的域所指向的是可變對象,那麼在訪問這些域所指向的對象的狀態時仍然需要同步。
3.5.3 安全發佈的常用模式
要安全地發佈一個對象,對象的引用以及對象的狀態必須同時對其他線程可見。一個正確構造的對象可以通過以下方式來安全的發佈:
1.在靜態初始化函數中初始化一個對象引用
2.將對象的引用保存到volatile類型的域或者AtomicReference對象中
3.將對象的引用保存到某個正確構造對象的final類型域中
4.將對象的引用保存到一個由鎖保護的域中(包含 線程安全容器內部的同步)

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

1.通過將一個鍵或者值放入HashTable,synchronizedMap或者ConcurrentMap中,可以安全地將它發佈給任何從這些容器中訪問它的線程(無論是直接訪問還是通過迭代器訪問)

2.通過將某個元素放入Vector,CopyOnWriteArrayList,CopyOnWriteArraySet,synchronizedList或synchronizedSet中,可以將該元素安全地帆布到任何從這些容器中訪問該元素的線程

3.通過將某個元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以將該元素安全地發佈到任何從這些隊列中訪問該元素的線程

類庫中的其他數據傳遞機制(例如Future和Exchanger)同樣能實現安全發佈。

 

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

public static Holder holder = new Holder(42);

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

 

3.5.4 事實不可變對象

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

如果對象從技術上來看是可變的,但其狀態再發布後不會再改變,那麼把這種對象稱爲"事實不可變對象(Effectively Immutable Object)"。

當滿足以下條件時,對象纔是不可變的:
1.對象創建以後其狀態就不能修改
2.對象的所有域都是final類型
3.對象是正確創建的(在對象的創建期間,this引用沒有逸出)

事實不可變對象不需要滿足不可變性的嚴格定義。在這些對象發佈後,程序只需將它們視爲不可變對象即可。在沒有額外同步的情況下,任何線程都可以安全地使用被安全發佈的事實不可變對象。

eg: Date本身是可變的,但如果將它作爲不可變對象來使用嗎,那麼在多個線程之間共享Date對象時,就可以省去對鎖的使用。
public Map<String,Date> lastLogin = Collections.synchronizedMap(new HashMap<String,Date>());


3.5.5 可變對象
如果對象在構造後可以修改,那麼安全發佈只能確保"發佈當時"狀態的可見性。對於可變對象,不僅在發佈對象時需要使用同步,而且在每次對象訪問時同樣需要使用同步來確保後續修改操作的可見性。要安全的共享可變對象,這些對象就必須被安全地發佈,並且必須是線程安全的或者由某個鎖保護起來。

對象的發佈需求取決於它的可變性:
1.不可變對象可以通過任意機制來發布
2.事實不可變對象必須通過安全方式來發布
3.可變對象必須通過安全的方式來發布,並且必須是線程安全的或者由某個鎖保護起來

 

3.5.6 安全地共享對象

當獲得對象的一個引用時,你需要知道在這個引用上可以執行哪些操作。在使用它之前是否需要獲得一個鎖?是否可以修改它的狀態,或者只能讀取它?許多併發錯誤都由於沒有理解共享對象的這些"既定規則"而導致的。當發佈一個對象時,必須明確地說明對象的訪問方式。

在併發程序中使用和共享對象時,可以使用一些實用的策略,包括:
1.線程封閉。 線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,並且只能由這個線程修改。
2.只讀共享。 在沒有額外同步的情況下,共享的只讀對象可以由多個線程併發訪問。但任何線程都不能修改它。共享的只讀對象包括不可變對象和事實不可變對象。
3.線程安全共享。 線程安全的對象在其內部實現同步,因此多個線程可以通過對象的公有接口來進行訪問而不需要進一步的同步。
4.保護對象。 被保護的對象只能通過持有特定的鎖來訪問。保護對象也包括封裝在其他線程安全對象中的對象,以及已發佈的並且由某個特定鎖保護的對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章