使用:
僅當volatile變量能簡化代碼的實現以及對同步策略的驗證時,才應該使用它們。如果在驗證正確性時需要對可見性進行復雜的判斷,那麼就不要使用volatile變量。volatile變量的正確使用方式包括:確保它們自身狀態的可見性,確保它們所引用對象的狀態的可見性,以及標識一些重要的程序生命週期事件的發生(例如,初始化或關閉)
volatile變量的一種典型用法:檢查某個專題標記以判斷是否退出循環,eg:
volatile boolean asleep;
...
while(!asleep)
countSomeSheep();
...
當且僅當滿足以下所有條件時,才應該使用 volatile 變量:
1. 對變量的寫入操作不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
2. 該變量不會與其他狀態變量一起納入不變性條件中。
3. 在訪問變量時不需要加鎖
當訪問共享的可變數據時,通常需要使用同步。一種避免使用同步的方式就是不共享數據。如果僅在單線程內訪問數據就不需要同步。這種技術被稱爲線程封閉(Thread Confinement)。
它是實現線程安全的最簡單方式之一。
Ad-hoc線程封閉
Ad-hoc線程封閉是指,維護線程封閉性的職責完全由程序來承擔。Ad-hoc線程封閉是非常脆弱的,因此在程序中儘量少用,在可能的情況下,應該使用更強的線程封閉技術(例如,棧封閉 或 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變量類似於全局變量,它能降低代碼的可重用行,並在類之間引入隱含的耦合性,因此在使用時要格外小心。
當滿足以下條件時,對象纔是不可變的:
1.對象創建以後其狀態就不能修改
2.對象的所有域都是final類型
3.對象是正確創建的(在對象的創建期間,this引用沒有逸出)
@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);
}
}
另一方面,即使在發佈不可變對象的引用時沒有使用同步,也仍然可以安全地訪問該對象,爲了維持這種初始化安全性的保證,必須滿足不可變性的所有需求:狀態不可修改,所有域都是final類型,以及正確的構造過程。
這種保證還將延伸到被正確創建對象中所有final類型的域。在沒有額外同步的情況下,也可以安全地訪問final類型的域。然而,如果final類型的域所指向的是可變對象,那麼在訪問這些域所指向的對象的狀態時仍然需要同步。
要安全地發佈一個對象,對象的引用以及對象的狀態必須同時對其他線程可見。一個正確構造的對象可以通過以下方式來安全的發佈:
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>());
如果對象在構造後可以修改,那麼安全發佈只能確保"發佈當時"狀態的可見性。對於可變對象,不僅在發佈對象時需要使用同步,而且在每次對象訪問時同樣需要使用同步來確保後續修改操作的可見性。要安全的共享可變對象,這些對象就必須被安全地發佈,並且必須是線程安全的或者由某個鎖保護起來。
對象的發佈需求取決於它的可變性:
1.不可變對象可以通過任意機制來發布
2.事實不可變對象必須通過安全方式來發布
3.可變對象必須通過安全的方式來發布,並且必須是線程安全的或者由某個鎖保護起來
當獲得對象的一個引用時,你需要知道在這個引用上可以執行哪些操作。在使用它之前是否需要獲得一個鎖?是否可以修改它的狀態,或者只能讀取它?許多併發錯誤都由於沒有理解共享對象的這些"既定規則"而導致的。當發佈一個對象時,必須明確地說明對象的訪問方式。
在併發程序中使用和共享對象時,可以使用一些實用的策略,包括:
1.線程封閉。 線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,並且只能由這個線程修改。
2.只讀共享。 在沒有額外同步的情況下,共享的只讀對象可以由多個線程併發訪問。但任何線程都不能修改它。共享的只讀對象包括不可變對象和事實不可變對象。
3.線程安全共享。 線程安全的對象在其內部實現同步,因此多個線程可以通過對象的公有接口來進行訪問而不需要進一步的同步。
4.保護對象。 被保護的對象只能通過持有特定的鎖來訪問。保護對象也包括封裝在其他線程安全對象中的對象,以及已發佈的並且由某個特定鎖保護的對象。