同步的作用
- 確保復合操作的原子性(復合操線程間作互斥)
- 內(nèi)存可見性
volatile
- 作用:將當前線程對volatile的改變立即通知給其他線程酪惭;保證了volatile變量對線程的可見性;volatile是一種比synchronizyed稍弱的同步機制
- 對可見性的影響:volatile變量對可見性的影響比volatile變量本身更為重要。當線程A首先寫入一個volatile變量并且線程B隨后讀取該變量時谤职,在寫入volatile變量之前對A可見的所有變量(包括volatile變量)的值童芹,在B讀取了volatile變量后披泪,對B也是可見的。因此胞此,從內(nèi)存可見性的角度來看,寫入volatile變量相當于退出同步代碼塊跃捣,而讀取volatile變量就相當于進入同步代碼塊
- 典型用法:檢查某個狀態(tài)標記以判斷是否退出循環(huán)
- 注意:volatile的語義不足以確保遞增操作(count++)的原子性漱牵,除非你能確保只有一個線程對變量執(zhí)行寫操作;原子變量提供了“讀-改-寫”的原子操作疚漆,并且嘗嘗做一種“更好的volatile變量”酣胀;即volatile只能確钡笊猓可見性,而加鎖機制既可以確蔽畔猓可見性又可以確保原子性
- 使用volatile變量的條件
- 對變量的寫入操作不依賴變量的當前值甚脉,或者你能確定只有單個線程更新變量的值
- 該變量不會與其他變量一起納入不變性條件中
- 在訪問變量時不需要加鎖
對象發(fā)布與溢出
- 發(fā)布(Publish):使對象能夠在當前作用于之外的代碼中使用
- 溢出(Escape):當某個不應該發(fā)布的對象被發(fā)布時就成為溢出
- 發(fā)布方式
//方式一:將指向?qū)ο蟮囊帽4娴狡渌a能訪問到的地方 //發(fā)布了new HashSet<Secret>()對象 public static Set<Secret> knowSecrets; public void initialize() { konwnSecretets = new HashSet<Secret>(); } //方式二:在某一個非私有的方法中返回對象引用 //發(fā)布了new HashSet<Secret>()對象 public Set<Secret> getSecrets() { return new HashSet<Secret>(); } //方式三:將對象的引用傳遞到其他類的方法中 //發(fā)布了new User()對象 public class Caculate { public static Object caculate(Object o) { System.out.println(o); return o; } } Caculate.caculate(new User()); //方式四:在已發(fā)布的對象中的非私有域中引用對象 //發(fā)布了"AK","AL" class UnsafeStates { private String[] states = new String[] {"AK", "AL"}; public String[] getStates() { return states; } } //方式五:在類的方法內(nèi)發(fā)布匿名內(nèi)部類铆农;因為匿名內(nèi)部類包含了當前對象的隱含引用this牺氨,隨意發(fā)布匿名內(nèi)部類時也發(fā)布了自己 //發(fā)布了new EventListener(),在該對象內(nèi)部包含自己的隱試引用this墩剖,即當前ThisEscape對象 public class ThisEscape { private int value; public ThisEscape(EventSource source) { //此處已經(jīng)將ThisEscape發(fā)布給了外部的source,存在逃逸現(xiàn)象 source.registerListener(new EventListener() { public void onEvent(Event e) { doSomething(e); } }); //尚未構造完成 value = 10; } private void doSomething(Object o) { System.out.println(value); } }
4.對象發(fā)布的風險:無論其他線程會對已發(fā)布的引用執(zhí)行何種操作猴凹,其實都不重要,因為誤用該引用的風險始終存在涛碑。當某個對象逸出后精堕,你必須假設有某各類或者線程可能會誤用該對象。這正是需要使用封裝的最主要原因:封裝能夠使得對程序的正確性分析變得可能蒲障,并使得無意中破壞設計約束條件變得更難
安全的對象構造過程
- 不正確構造
- 概念:如果this引用在對象構造過程中逸出歹篓,那么這種對象就被認為是不正確構造
- 原因:當且僅當對象的構造函數(shù)返回時,對象才處于可預測的和一致的狀態(tài)揉阎。因此庄撮,當從構造函數(shù)中發(fā)布對象時,只是發(fā)布了一個尚未構造完成的對象毙籽;即使發(fā)布對象的語句位于構造函數(shù)的最后一行也是如此
- 經(jīng)驗:不要在構造過程中使this引用逸出洞斯;即在構造中不要發(fā)布對象
- 常見不正確構造
- 在構造函數(shù)中發(fā)布對象
- 在構造函數(shù)中啟動線程
- 在構造函數(shù)中調(diào)用當前類可改寫的實例方法(既不是私有方法,也不是最終方法)
- 解決不安全構造的方法
/** * 用一個私有的構造函數(shù)和一個工友的工廠方法 * 構造函數(shù)用來實例化對象 * 工廠方法發(fā)布已經(jīng)構造完的this坑赡,并返回這個構造完的實例 */ public class SafeListener { private final EventListener listener; private SafeListener() { listener = new EventListener() { public void onEvent(Event e) { doSomething(e); } }; } public static SafeListener newInstance(EventSource source) { SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe; } }
線程封閉
- 概念:當訪問共享的可變數(shù)據(jù)時烙如,通常需要同步;一種避免使用同步的方式就是不共享數(shù)據(jù)毅否;如果僅在單線程內(nèi)訪問數(shù)據(jù)亚铁,就不需要同步;這就叫線程封閉
- 作用:當某個對象封閉在一個線程中時螟加,這種用法將自動實現(xiàn)線程安全性徘溢,即使被封裝的對象本身不是線程安全的;線程封閉是實現(xiàn)線程安全的最簡單方式之一
- 線程封閉的方式
- Ad-hoc線程封閉:維護線程封閉性的職責完全由程序?qū)崿F(xiàn)來承擔
- 由與Ad-hoc線程封閉技術的脆弱性捆探,因此在程序中盡量少用它然爆,在可能的情況下,應該使用更強的線程封閉技術(例如黍图,棧封閉或ThreadLocal類)
- 棧封閉:棧封閉是一種特殊的線程封閉曾雕,在棧封閉中將對象定義為局部變量,局部變量的固有屬性之一就是封閉在執(zhí)行線程中
- 基本類型的局部變量始終封閉在線程內(nèi)雌隅,因為任何方法都無法獲得基本類型的引用
- 引用類型的局部變量翻默,為了維持線程封閉缸沃,需要多做一些工作以確保被引用的對象不會溢出當前線程
- ThreadLocal
- Ad-hoc線程封閉:維護線程封閉性的職責完全由程序?qū)崿F(xiàn)來承擔
不變性
- 不可變對象:如果某個對象在被創(chuàng)建后其狀態(tài)就不能被修改,那么這個對象就成為不可變對象
- 不可變對象固有屬性:線程安全性是不可變對象的固有屬性之一修械,他們的不變性條件是由構造函數(shù)創(chuàng)建的趾牧,只要他們的狀態(tài)不改變,那么這些不變性條件就能得以維持
- 不可變對象的條件
- 對象創(chuàng)建以后其狀態(tài)就不能修改
- 對象的所有域都是final類型
- 對象是正確構造的(在對象構造期間肯污,this引用沒有逸出)
- 習慣:正如“除非需要更高的可見性翘单,否則應將所有的域都聲明為私有域”是一個良好的編程習慣,“除非需要某個域是可變的蹦渣,否則應將其聲明為final域”也是一個良好的編程習慣
- final域可見性詳解: http://www.infoq.com/cn/articles/java-memory-model-6
使用volatile類型來發(fā)布不可變對象
- 應用場景:需要對一組相關的數(shù)據(jù)以原子的方式執(zhí)行某個操作哄芜;不適用當前狀態(tài)依賴上一個操作時的狀態(tài)的場景,例如i++操作
- 方式:通過使用包含多個相關狀態(tài)變量的容器對象來維持不變性條件柬唯,并使用volatile類型的引用來確比想可見性,就能使該操作在沒有顯示地使用鎖的情況下仍然是線程安全的
- 示例
/** * 注意:在構造和返回不變狀態(tài)對象時要和線程局部變量斷開關聯(lián)锄奢,尤其是引用變量 * 下面注釋的兩個Arrays.copyOf操作就是 */ class OneValueCache { private final BigInteger lastNumber; private final BigInteger[] lastFactors; /** * 狀態(tài)由構造函數(shù)創(chuàng)建 */ public OneValueCache(BigInteger i, BigInteger[] factors) { lastNumber = i; //復制原因:防止factors被別的線程修改 //防止線程用factors構造完OneValueCache之后修改factors失晴,不會影響緩存數(shù)據(jù)lastFactors lastFacotrs = Arrays.copyOf(factors, factors.length); } public BigInteger[] getFactors(BigInteger i) { if(lastNumber == null || !lastNumber.equals(i)) return null; else //復制原因:防止lastFacors被別的線程修改 //返回一個新的結(jié)果數(shù)組給線程,線程想怎么處理怎么處理拘央,不會影響緩存lastFactors return Arrays.copyOf(lastFacors, lastFactors.length); } } public class VolatileCachedFactorizer implements Servlet { private volatile OneValueCache cache = new OneValueCache(null, null); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = exractFromRequest(req); //結(jié)果從不可變狀態(tài)對象獲取 BigInteger factors = cache.getFactors(i); if(factors == null) { factors = factor(i); //構造新的不可變狀態(tài)對象 cache = new OneValueCache(i, factors); } encodeIntoResponse(resp, factors); } }
安全發(fā)布對象的方法
- 前提:對象被正確構造
- 安全發(fā)布機制:
- 在靜態(tài)代碼塊或者靜態(tài)域中初始化一個對象的引用
- 靜態(tài)初始化器有JVM在類的初始化階段執(zhí)行涂屁。由與在JVM內(nèi)部存在著同步機制,因此通過這種方式初始化的任何對象都可以被安全地發(fā)布
- 將對象的引用保存到volatile類型的域
- 將對象的引用保存到AtomicReference類型的域中
- 將對象的引用保存到final類型的域中
- 將對象的引用保存到一個由鎖保護的域中
- 通過線程安全庫的容器類發(fā)布對象
- 在靜態(tài)代碼塊或者靜態(tài)域中初始化一個對象的引用
- 線程安全庫提供的安全發(fā)布保證
- 通過將一個鍵或者值放入Hashtable灰伟、synchronizecdMap或者ConcurrentMap中拆又,可以安全地將它發(fā)布給任何從這些容器中訪問它的線程(無論是直接訪問還是通過迭代器訪問)
- 通過將某個元素放入Vector、CopyOnWriteArrayList栏账、CopyOnWriteArraySet帖族、synchronizedList或synchronizedSet中,可以將元素安全地發(fā)布到任何從這些容器中訪問元素的線程
- 通過將某個元素放入BlockingQueue或者ConcurrentLinkedQueue中挡爵,可以將元素安全地發(fā)布到任何從這些隊列中訪問元素的線程
- 類庫中的其他數(shù)據(jù)傳遞機制(例如Futrue和Exchanger)同樣能實現(xiàn)安全發(fā)布
事實不可變對象
- 概念:如果對象從技術上來看是可變的盟萨,但其狀態(tài)在發(fā)布后不會再改變,那么這種對象稱為“事實不可變對象(Effectively Immutable Object)”
- 結(jié)論1:在沒有額外同步的情況下了讨,任何線程都可以安全地使用被安全發(fā)布的事實不可變對象
- 結(jié)論2:通過使用事實不可變對象,不僅可以簡化開發(fā)過程制轰,而且還能由于減少了同步而提高性能
對象的發(fā)布需求取決于它的可變性
- 不可變對象可以通過任意機制來發(fā)布
- 事實不可變對象必須通過安全的發(fā)布機制來發(fā)布
- 可變對象必須通過安全的發(fā)布機制來發(fā)布前计,并且必須是線程安全的或者有某個鎖保護起來
并發(fā)編程中共享對象的一些使用策略
- 線程封閉:線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中垃杖,并且只能由這個線程修改
- 只讀共享:在沒有額外同步的情況下男杈,共享的只讀對象可以有多個線程并發(fā)訪問,但任何線程都不能修改它调俘。共享的只讀對象包括不可變對象和事實不可變對象
- 線程安全共享:線程安全的對象在其內(nèi)部實現(xiàn)同步伶棒,因此多個線程可以通過對象的公有接口進行訪問而不需要進一步的同步
- 保護對象:被保護的對象只能通過持有特定的鎖來訪問旺垒。保護對象包括封裝在其他安全對象中的對象,以及已發(fā)布的并且有某個特定的鎖保護的對象