探索併發程式設計(四)------Java併發工具
基於執行緒安全的一些原則來程式設計當然可以避免併發問題,但不是所有人都能寫出高質量的執行緒安全的程式碼,並且如果程式碼裡到處都是執行緒安全的控制也極大地影響了程式碼可讀性和可維護性。因此,Java平臺為了解決這個問題,提供了很多執行緒安全的類和併發工具,通過這些類和工具就能更簡便地寫執行緒安全的程式碼。歸納一下有以下幾種:
- 同步容器類
- 併發容器類
- 生產者和消費者模式
- 阻塞和可中斷方法
- Synchronizer
這些類和方法的使用都可以從JDK DOC查到,但在具體使用中還是有很多問題需要注意
同步容器類
同步容器類就是一些經過同步處理了的容器類,比如List有Vector,Map有Hashtable,檢視其原始碼發現其保證執行緒安全的方式就是把每個對外暴露的存取方法用synchronized關鍵字同步化,這樣做我們立馬會想到有以下問題:
1)效能有問題
同步化了所有存取方法,就表明所有對這個容器物件的操作將會序列,這樣做來得倒是乾淨,但效能的代價也是很可觀的
2)複合操作問題
同步容器類只是同步了單一操作,如果客戶端是一組複合操作,它就沒法同步了,依然需要客戶端做額外同步,比如以下程式碼:
getLast和deleteLast都是複合操作,由先前對原子性的分析可以判斷,這依然存線上程安全問題,有可能會丟擲ArrayIndexOutOfBoundsException的異常,錯誤產生的邏輯如下所示:
解決辦法就是通過對這些複合操作加鎖
3)迭代器併發問題
Java Collection進行迭代的標準時使用Iterator,無論是使用老的方式迭代迴圈,還是Java 5提供for-each新方式,都需要對迭代的整個過程加鎖,不然就會有Concurrentmodificationexception異常丟擲。
此外有些迭代也是隱含的,比如容器類的toString方法,或containsAll, removeAll, retainAll等方法都會隱含地對容器進行迭代
併發容器類
正是由於同步容器類有以上問題,導致這些類成了雞肋,於是Java 5推出了併發容器類,Map對應的有ConcurrentHashMap,List對應的有CopyOnWriteArrayList。與同步容器類相比,它有以下特性:
- 更加細化的鎖機制。同步容器直接把容器物件做為鎖,這樣就把所有操作序列化,其實這是沒必要的,過於悲觀,而併發容器採用更細粒度的鎖機制,保證一些不會發生併發問題的操作進行並行執行
- 附加了一些原子性的複合操作。比如putIfAbsent方法
- 迭代器的弱一致性。它在迭代過程中不再丟擲Concurrentmodificationexception異常,而是弱一致性。在併發高的情況下,有可能size和isEmpty方法不準確,但真正在併發環境下這些方法也沒什麼作用。
- CopyOnWriteArrayList採用寫入時複製的方式避開併發問題。這其實是通過冗餘和不可變性來解決併發問題,在效能上會有比較大的代價,但如果寫入的操作遠遠小於迭代和讀操作,那麼效能就差別不大了
生產者和消費者模式
大學時學習作業系統多會為生產者和消費者模式而頭痛,也是每次考試肯定會涉及到的,而Java知道大家很憷這個模式的併發複雜性,於是乎提供了阻塞佇列(BlockingQueue)來滿足這個模式的需求。阻塞佇列說起來很簡單,就是當隊滿的時候寫執行緒會等待,直到佇列不滿的時候;當隊空的時候讀執行緒會等待,直到隊不空的時候。實現這種模式的方法很多,其區別也就在於誰的消耗更低和等待的策略更優。以LinkedBlockingQueue的具體實現為例,它的put原始碼如下:
撇開其鎖的具體實現,其流程就是我們在作業系統課上學習到的標準生產者模式,看來那些枯燥的理論還是有用武之地的。其中,最核心的還是Java的鎖實現,有興趣的朋友可以再進一步深究一下
阻塞和可中斷方法
由LinkedBlockingQueue的put方法可知,它是通過執行緒的阻塞和中斷阻塞來實現等待的。當呼叫一個會丟擲InterruptedException的方法時,就成為了一個阻塞的方法,要為響應中斷做好準備。處理中斷可有以下方法:
- 傳遞InterruptedException。把捕獲的InterruptedException再往上拋,使其呼叫者感知到,當然在拋之前需要完成你自己應該做的清理工作,LinkedBlockingQueue的put方法就是採取這種方式
- 中斷其執行緒。在不能丟擲異常的情況下,可以直接呼叫Thread.interrupt()將其中斷。
Synchronizer
Synchronizer不是一個類,而是一種滿足一個種規則的類的統稱。它有以下特性:
- 它是一個物件
- 封裝狀態,而這些狀態決定著執行緒執行到某一點是通過還是被迫等待
- 提供操作狀態的方法
其實BlockingQueue就是一種Synchronizer。Java還提供了其他幾種Synchronizer
1)CountDownLatch
CountDownLatch是一種閉鎖,它通過內部一個計數器count來標示狀態,當count>0時,所有呼叫其await方法的執行緒都需等待,當通過其countDown方法將count降為0時所有等待的執行緒將會被喚起。使用例項如下所示:
2)Semaphore
Semaphore類實際上就是作業系統中談到的訊號量的一種實現,其原理就不再累述,可見探索併發程式設計------作業系統篇
具體使用就是通過其acquire和release方法來完成,如以下示例:
3)關卡
關卡和閉鎖類似,也是阻塞一組執行緒,直到某件事情發生,而不同在於關卡是等到符合某種條件的所有執行緒都達到關卡點。具體使用上可以用CyclicBarrier來應用關卡
以上是Java提供的一些併發工具,既然是工具就有它所適用的場景,因此需要知道它的特性,這樣才能在具體場景下選擇最合適的工具。