換了個markdown的編輯器,感受挺方便的,可是手機端的格式顯示不正確,若是讀者是手機端用戶,點擊右上角做者主頁查看,就能夠了html
前文回顧
經過博主以前發佈的兩篇博客從零開始學多線程之線程安全(一)和從零開始學多線程之共享對象(二)講解的知識點,咱們如今已經能夠構建線程安全的類了,本篇將爲您介紹構建類的模式,這些模式讓類更容易成爲線程安全的,而且不會讓程序意外破壞這些類的線程安全性.java
本篇博客將要講解的知識點
- 構建線程安全類要關注那些因素.
- 使用實例限制+鎖的模式,使非線程安全的對象,能夠被併發的訪問。
- 擴展一個線程安全的類的四種方式
構建線程安全的類
咱們已經知道多線程操縱的類必須是線程安全的,不然會引起種種問題,那麼如何設計線程安全的類呢?咱們能夠從如下三個方面考慮:設計模式
- 肯定對象狀態是由哪些變量構成的;
- 肯定限制狀態變量的不變約束;
- 制定一個管理併發訪問對象狀態的策略
當咱們想要建立一個線程安全的類的時候,首先要關注的就是這個類的成員變量是否會被發佈,若是被髮布,那麼就要根據對象的可變性(可變對象、不可變對象、高效不可變對象)去決定如何發佈這個對象(若是不明白安全發佈的概念,請移駕從零開始學多線程之共享對象(二))安全
而後再看狀態是否依靠外部的引用實例化:若是一個對象的域引用了其餘對象,那麼它的狀態也同時包含了被引用對象的域.markdown
public class Domain { private Object obj; public Domain(Object obj) { this.obj = obj; } }
這時候就要保證傳入的obj對象的線程安全性.不然obj對象在外部被改變,除修改線程之外的線程,不必定能感知到對象已經被改變,就會出現過時數據的問題.多線程
咱們應該儘可能使用final修飾的域,這樣能夠簡化咱們對對象的可能狀態進行分析(起碼保證只能指向一塊內存地址空間).併發
而後咱們再看類的狀態變量是否涉及不變約束,並要保護類的不變約束編輯器
public class Minitor { private long value = 0; public synchronized long getValue(){ return value; } public synchronized long increment(){ if(value == Long.MAX_VALUE){ throw new IllegalStateException(" counter overflow"); } return ++value; } }
咱們經過封裝使狀態value沒有被髮布出去,這樣就杜絕了客戶端代碼將狀態置於非法的情況,保護了不變約束if(value == Long.MAX_VALUE).工具
維護類的線程安全性意味着要確保在併發訪問的狀況下,保護它的不變約束;這須要對其狀態進行判斷.性能
increment()方法,是讓value++進行一次自增操做,若是value的當前值是17,那麼下一個合法值是18,若是下一狀態源於當前狀態,那麼操做必須是原子操做.
這裏涉及到線程安全的可見性與原子性問題,若是您對此有疑問請移駕從零開始學多線程之線程安全(一)
實例限制
一個非線程安全的對象,經過實例限制+鎖,可讓咱們安全的訪問它.
實例限制:把非線程安全的對象包裝到自定義的對象中,經過自定義的對象去訪問非線程安全的對象.
public class ProxySet { private Set<String> set = new HashSet<>(); public synchronized void add(String value){ set.add(value); } public synchronized boolean contains(String value){ return set.contains(value); } }
HashSet是非線程安全的,咱們把它包裝進自定義的ProxySet類,只能經過ProxySet加鎖的方法操做集合,這樣HashSet又是線程安全的了.
若是咱們把訪問修飾符改成public的,那麼這個集合仍是線程安全的嗎?
public Set<String> set = new HashSet<>();
這時候其它線程就能夠獲取到這個set集合調用add(),那麼Proxyset的鎖就沒法起到做用了.因此他又是非線程安全的了.因此咱們必定不能讓實例限制的對象逸出.
將數據封裝在對象內部,把對數據的訪問限制在對象的方法上,更易確保線程在訪問數據時總能得到正確的鎖
實例限制使用的是監視器模式,監視器模式的對象封裝了全部的可變狀態,並由本身的內部鎖保護.(完成多線程的博客後,博主就會更新關於設計模式的博客).
擴展一個線程安全的類
咱們使用Java類庫提供的方法能夠解決咱們的大部分問題,可是有時候咱們也須要擴展java提供的類沒有的方法.
如今假設咱們要給同步的list集合,擴展一個缺乏即加入的方法(必須保證這個方法是線程安全的,不然可能某一時刻會出現加入兩個同樣的值).
咱們有四種方法能夠實現這個功能:
- 修改原始的類
- 擴展這個類(繼承)
- 擴展功能而,不是擴展類自己(客戶端加鎖,在調用這個對象的地方,使用對象的鎖確保線程安全)
- 組合
咱們一個一個來分析以上方法的利弊.
1.修改原始的類:
優勢: 最安全的方法,全部實現類同步策略的代碼仍然包含在要給源代碼文件中,所以便於理解與維護. 缺點:可能沒法訪問源代碼或沒有修改的自由.
2.擴展這個類:
優勢:方法至關簡單直觀. 缺點:並不是全部類都給子類暴露了足夠多的狀態,以支持這種方案,還有就是同步策略的 實現會被分佈到多個獨立維護的源代碼文件中,因此擴展一個類比直接在類中加入代碼更脆弱.若是底層的類選擇了 不一樣的鎖保護它的狀態變量,從而會改變它的同步策略,子類就在不知不覺中被破壞, 由於他不能再用正確的鎖控制對基類狀態的併發訪問.
3.擴展功能而,不是擴展類自己:
public class Lock { public List<String> list = Collections.synchronizedList(new ArrayList<String>()); public synchronized boolean putIfAbsent(String value){ boolean absent = !list.contains(value); if(!absent){ list.add(value); } return absent; } }
這個方法是錯的.使用synchronized關鍵字雖然同步了缺乏即加入方法, 並且使用list也是線程安全的,可是他們用的不是同一個鎖,list因爲pulic修飾符,任意的線程均可以調用它.那麼在某一時刻,知足if(!absent)不變約束的同時準備add()這個對象的時候,已經有另外一個線程經過lock.list.add()過這個對象了,因此仍是會出現add()兩個相同對象的狀況.
正確的代碼,要確保他們使用的是同一個鎖:
public class Lock { public List<String> list = Collections.synchronizedList(new ArrayList<String>()); public boolean putIfAbsent(String value){ synchronized(list){ boolean absent = !list.contains(value); if(!absent){ list.add(value); } return absent; } } }
如今都使用的是list對象的鎖,因此也就不會出現以前的狀況了.
這種方式叫客戶端加鎖.
優勢: 比較簡單.
缺點: 若是說爲了添加另外一個原子操做而去擴展一個類容易出問題,是由於它將加鎖的代碼分佈到對象繼承體系中的多個類中.然而客戶端加鎖實際上是更加脆弱的,由於他必須將類C中的加鎖代碼(locking code)置入與C徹底無關的類中.在那些不關注鎖策略的類中使用客戶端加鎖時,必定要當心
客戶端加鎖與擴展類有不少共同之處--所得類的行爲與基類的實現之間都存在耦合.正如擴展會破壞封裝性同樣,客戶端加鎖會破壞同步策略的封裝性.
- 組合對象:
public class ImprovedList<T> implements List<T> { private final List<T> list; public ImprovedList(List<T> list) { this.list = list; } public synchronized boolean putIfAbsent(Object obj){ boolean absent = list.contains(obj); if(absent){ list.add((T) obj); } return absent; } }
經過ImprovedList對象來操做傳進來的list對象,用的都是Improved的鎖.即便傳進來的list不是線程安全的,ImprovedList也能保證線程安全.
優勢:相比以前的方法,這種方式提供了更健壯的代碼.
缺點:額外的同步帶來一些微弱的性能損失.
總結
本篇博客咱們講解了,要設計線程安全的類要從三個方面考慮:
- 肯定對象狀態是由哪些變量構成的;
- 肯定限制狀態變量的不變約束;
- 制定一個管理併發訪問對象狀態的策略
對於非線程安全的對象,咱們能夠考慮使用鎖+實例限制(Java監視器模式)的方式,安全的訪問它們.
咱們還學會了如何擴展一個線程安全的的類:擴展有四法,組合是最佳.
下一篇博客,我會爲介紹幾種經常使用的線程安全容器和同步工具.來構建線程安全的類.
好了本篇博客就分享到這裏,咱們下篇再見.