探索併發編程(四)------Java併發工具

基於線程安全的一些原則來編程當然可以避免併發問題,但不是所有人都能寫出高質量的線程安全的代碼,並且如果代碼裏到處都是線程安全的控制也極大地影響了代碼可讀性和可維護性。因此,Java平臺爲了解決這個問題,提供了很多線程安全的類和併發工具,通過這些類和工具就能更簡便地寫線程安全的代碼。歸納一下有以下幾種:

  • 同步容器類
  • 併發容器類
  • 生產者和消費者模式
  • 阻塞和可中斷方法
  • Synchronizer

這些類和方法的使用都可以從JDK DOC查到,但在具體使用中還是有很多問題需要注意

同步容器類

同步容器類就是一些經過同步處理了的容器類,比如List有Vector,Map有Hashtable,查看其源碼發現其保證線程安全的方式就是把每個對外暴露的存取方法用synchronized關鍵字同步化,這樣做我們立馬會想到有以下問題:
1)性能有問題

同步化了所有存取方法,就表明所有對這個容器對象的操作將會串行,這樣做來得倒是乾淨,但性能的代價也是很可觀的

2)複合操作問題

同步容器類只是同步了單一操作,如果客戶端是一組複合操作,它就沒法同步了,依然需要客戶端做額外同步,比如以下代碼:

[java] 
  1. public static Object getLast(Vector list) {  
  2.     int lastIndex = list.size() - 1;  
  3.     return list.get(lastIndex);  
  4. }  
  5. public static void deleteLast(Vector list) {  
  6.     int lastIndex = list.size() - 1;  
  7.     list.remove(lastIndex);  
  8. }  

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] 
  1. public void put(E e) throws InterruptedException {  
  2.     if (e == nullthrow new NullPointerException();  
  3.     int c = -1;  
  4.     final ReentrantLock putLock = this.putLock;  
  5.     final AtomicInteger count = this.count;  
  6.     putLock.lockInterruptibly();  
  7.     try {  
  8.         try {  
  9.             while (count.get() == capacity)  
  10.                 notFull.await();  
  11.         } catch (InterruptedException ie) {  
  12.             notFull.signal(); // propagate to a non-interrupted thread  
  13.             throw ie;  
  14.         }  
  15.         insert(e);  
  16.         c = count.getAndIncrement();  
  17.         if (c + 1 < capacity)  
  18.             notFull.signal();  
  19.     } finally {  
  20.         putLock.unlock();  
  21.     }  
  22.     if (c == 0)  
  23.         signalNotEmpty();  
  24. }  

撇開其鎖的具體實現,其流程就是我們在操作系統課上學習到的標準生產者模式,看來那些枯燥的理論還是有用武之地的。其中,最核心的還是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時所有等待的線程將會被喚起。使用實例如下所示:

[java] 
  1. public class TestHarness {  
  2.     public long timeTasks(int nThreads, final Runnable task)  
  3.             throws InterruptedException {  
  4.         final CountDownLatch startGate = new CountDownLatch(1);  
  5.         final CountDownLatch endGate = new CountDownLatch(nThreads);  
  6.         for (int i = 0; i < nThreads; i++) {  
  7.             Thread t = new Thread() {  
  8.                 public void run() {  
  9.                     try {  
  10.                         startGate.await();  
  11.                         try {  
  12.                             task.run();  
  13.                         } finally {  
  14.                             endGate.countDown();  
  15.                         }  
  16.                     } catch (InterruptedException ignored) { }  
  17.                 }  
  18.             };  
  19.             t.start();  
  20.         }  
  21.         long start = System.nanoTime();  
  22.         startGate.countDown();  
  23.         endGate.await();  
  24.         long end = System.nanoTime();  
  25.         return end-start;  
  26.     }  
  27. }  

2)Semaphore

Semaphore類實際上就是操作系統中談到的信號量的一種實現,其原理就不再累述

具體使用就是通過其acquire和release方法來完成,如以下示例:

[java] 
  1. public class BoundedHashSet<T> {  
  2.     private final Set<T> set;  
  3.     private final Semaphore sem;  
  4.     public BoundedHashSet(int bound) {  
  5.         this.set = Collections.synchronizedSet(new HashSet<T>());  
  6.         sem = new Semaphore(bound);  
  7.     }  
  8.     public boolean add(T o) throws InterruptedException {  
  9.         sem.acquire();  
  10.         boolean wasAdded = false;  
  11.         try {  
  12.             wasAdded = set.add(o);  
  13.             return wasAdded;  
  14.         }  
  15.         finally {  
  16.             if (!wasAdded)  
  17.                 sem.release();  
  18.         }  
  19.     }  
  20.     public boolean remove(Object o) {  
  21.         boolean wasRemoved = set.remove(o);  
  22.         if (wasRemoved)  
  23.             sem.release();  
  24.         return wasRemoved;  
  25.     }  
  26. }  

3)關卡

關卡和閉鎖類似,也是阻塞一組線程,直到某件事情發生,而不同在於關卡是等到符合某種條件的所有線程都達到關卡點。具體使用上可以用CyclicBarrier來應用關卡

 

以上是Java提供的一些併發工具,既然是工具就有它所適用的場景,因此需要知道它的特性,這樣才能在具體場景下選擇最合適的工具。

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