標籤:代碼 結束 timeout string 同步 expected null something mit
JDK 1.5 之前的同步容器
JDK 1.5 之前, 主要包括:
- 同步容器 (
Vector 和 Hashtable)
- 同步封裝類 (
Collections.synchronizedXxx)
這些類的共同特徵是, 公用方法都是由 synchronized 來修飾的, 以限制一次只能有一個線程能訪問容器.
同步容器中出現的問題複合操作
老的容器自身並不支援複合操作, 包括:
- 迭代(反覆擷取元素, 直到獲得容器中的最後一個元素)
- 導航(navigation, 根據一定的順序尋找下一個元素)
- 條件運算(check-then-act)
好在老的容器類遵循一個支援 用戶端加鎖 的同步策略. 來解決複合運算的問題:
解決迭代和導航:
synchronized(list) { // 確保調用 size() 後, list 大小不會改變 for (int i = 0; i < list.size(); ++i) { doSomething(list[i]); }}
解決條件運算:
synchronized(list) { // 確保調用 size() 後, list 大小不會改變 int lastIndex = list.size() - 1; list.remove(lastIndex);}
這樣做的弊端是:
做任何操作都要鎖住整個容器, 效率低, 容易出錯.
迭代器 和
ConcurrentModificationException
Collection進行迭代的標準方法是使用 Iterator, 無論是顯式使用還是 通過 JDK 1.5 之後的 for-each 文法.
在 迭代 的時候, 仍有其他線程在並發修改容器的可能性, 使用迭代器仍不可避免地需要在迭代期間對容器加鎖.
迭代器在並發修改的時候, 策略是 及時失敗(fail-fast) 的: 當發現迭代器被修改後(如: add 和 remove), 會拋出一個未檢查的 ConcurrentModificationException.
以 ArrayList 為例子, 其父類 AbstractList 內部有一個欄位名為 modCount 的計數器. 任何改變 List 大小的操作都需要改變 modCount 這個值.
這個值會被用來在迭代或者時, 檢查有沒有修改容器, 套路是這樣的:
修改時:
if (modCount != expectedModCount) throw new ConcurrentModificationException(); } // Add or Remove // ....... expectedModCount = modCount;
迭代:
public E prev/next() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } // Other..... }
Note: ConcurrentModificationException 也可以出現單線程的代碼中, 比如當在迭代期間調用 remove 方法
隱藏的迭代器
有時候, 一些操作會隱含的調用迭代器, 比如:
調用 toString() 方法, 尤其是寫 log 時, 有
log("Set:" + set);
這樣的語句.
hashCode 和 equals 方法, 以下是 HashTable 的 hashCode 和 equals 方法:
public synchronized boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Map)) return false; Map<?,?> t = (Map<?,?>) o; if (t.size() != size()) return false; try { Iterator<Map.Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) { Map.Entry<K,V> e = i.next(); K key = e.getKey(); V value = e.getValue(); if (value == null) { if (!(t.get(key)==null && t.containsKey(key))) return false; } else { if (!value.equals(t.get(key))) return false; } } } catch (ClassCastException unused) { return false; } catch (NullPointerException unused) { return false; } return true;}public synchronized int hashCode() { int h = 0; if (count == 0 || loadFactor < 0) return h; // Returns zero loadFactor = -loadFactor; // Mark hashCode computation in progress Entry<?,?>[] tab = table; for (Entry<?,?> entry : tab) { while (entry != null) { h += entry.hashCode(); entry = entry.next; } } loadFactor = -loadFactor; // Mark hashCode computation complete return h;}
另外 containAll, removeAll 和 retainAll 也會產生迭代.
JDK 1.5 之後的容器
JDK 1.5 後, 新增加了:
ConcurrentHashMap, 來替代同步的 Map 實現, 增加了 put-if-absent, 替換和條件刪除
CopyOnWriteArrayList, 是 List 相應的同步實現
Queue, 用來臨時儲存正在等待進一步處理的一系列元素, 實現包括
-
ConcurrentLinkedQueue, 一個傳統的 FIFO 隊列
PriorityQueue, 一個(非並發)居右優先順序順序的隊列
BlockingQueue, 拓展自 Queue, 增加了可阻塞的插入和擷取操作.
-
- 如果隊列是空的, 那麼擷取操作會被阻塞直到有元素存在;
- 如果隊列是滿的, 那麼插入操作會被阻塞直到有有元素被取出.
JDK 1.6 後, 新增加了
Deque 和 BlockingDeque, 分別擴充了 Queue 和 BlockingQueue:
-
Deque 介面, 實作類別是 ArrayDeque, 不阻塞
BlockingDeque 介面, 實作類別是 LinkedBlockingDeque, 阻塞.
ConcurrentSkipListMap 和 ConcurrentSkipListSet, 作為 SortedMap 和 SortedSet 的並發替代品
Note: 從一個空的Queue中取元素, 並不會阻塞, 而是返回 null
ConcurrentHashMap
在 ConcurrentHashMap 之前, HashTable 和 SynchronizedMap 都是通過給整個方法加 synchronized 來達到同步的, 這樣限制某一時刻只有一個線程可以訪問容器.
ConcurrentHashMap 使用一個更加細化的鎖機制, 名叫分離鎖. 這個機制允許更深層次的共用訪問:
- 任意數量的讀線程可以並發訪問 Map.
- 讀者和寫者可以並發訪問 Map.
- 有限數量的寫線程可以並發修改 Map.
由於並發環境中, Map 的大小通常是動態, size 和 isEmpty 返回的只是個估算值(可能返回後接著到期).
支援的複合操作:
- put-if-absent
- remove-if-equal
- replace-if-equal
CopyOnWriteArrayList
寫入時複製(COW)容器的安全執行緒原理:
只要不可邊對象被正確發布, 那麼訪問它將不需要更多的同步.
因此, 每次添加/修改一個元素, 容器內就會新建立一個新的數組, 容器底層的數組會指向這個新數組. 舊數組仍然被使用, 直到沒有引用後被 GC 回收.
由於 COW 複製數組有開銷, 所以 COW 適用於容器迭代操作遠遠高於對容器修改的頻率.
FAQ: Arrays.copyOf 和 System.arraycopy 區別?
Arrays.copyOf 不僅會複製元素, 還會建立新的數組. System.arrayCopy 拷貝到一個現有數組, Arrays.copyOf 實現中用了 System.arrayCopy;
阻塞隊列和生產者-消費者模式
生產者-消費者設計分離了 "識別需要完成的工作" 和 "執行工作". 該模式不會發現一個工作便立即處理, 而是把工作置入一個任務清單中:
- 生產者不需要知道消費者的身份或者數量, 甚至根本沒有消費者.
- 消費者也不需要知道生產者是誰, 以及是誰給它們安排的工作.
- 生產者和消費者的關係是相對的, 消費者可以成為下一個任務隊列的生產者
最常見的生產者-消費者設計是: 線程池和工作隊列的結合
在設計初期就使用阻塞隊列建立對資源的管理, 提早做這件事情會比日後再修複容易的多.
Blocking queue 提供了可阻塞的 put 和 take 方法. 常見的實現有:
LinkedBlockedQueue, FIFO, 鏈表實現, 隊列首 take, 隊列尾 put.
ArrayBlockingQueue, FIFO, 數組實現, 可以在 putIndex(隊列尾) 插入, 從 takeIndex(隊列首) 取出.
PriorityBlockingQueue, 根據 Comparator 排序次序取出
SynchronousQueue, 生產線程直接和消費線程對接, 如果生產線程找不到消費者或反之, 則, put 和 take 會一直阻止. 只有在消費者充足的時候比較適合, 他們總能為下一個任務做好準備.
雙端隊列和竊取工作
雙端隊列用來實現 竊取工作(work stealing) 模式.
在傳統的 生產者-消費者 設計中, 所有的消費者只共用一個工作隊列.
而在 竊取工作 設計中, 每一個消費者都有一個自己的雙端隊列. 如果一個消費者完成了自己雙端隊列中的全部工作, 它可以偷取其他消費者的雙端隊列的 末尾 任務(其他消費者仍然從隊列 首 取任務).
因為工作者線程並不會競爭一個共用的任務隊列, 所以 竊取工作 模式比傳統的 生產者-消費者 設計有更好的伸縮性.
阻塞和可中斷的方法
阻塞: 線程被掛起, 狀態變為BLOCKED, WAITING 或是 TIMED_WAITING, 等待直到一個事件發生才能繼續進行.
BlockingQueue 的 put 和 take 方法會拋出一個受檢查的 InterruptedException, 這個異常說明這是個阻塞方法, 可以被中斷來提前結束阻塞.
處理中斷的方法:
- 傳遞
InterruptedException. 傳遞給調用者, 可以對其中特定活動進行簡潔地清理後, 再拋出.
恢複中斷. 當代碼是 Runnable的一部分時, 必須捕獲 InterruptedException. 並且, 在當前線程中調用 interrupt 重新設定中斷狀態(拋出異常會清理中斷標誌位), 這樣調用棧中更高層代碼可以發現中斷已經發生.
try { processTask(queue.take());} catch (InterruptedException e) { // 恢複中斷狀態 Thread.currentThread().interrupt();}
Synchronizer
Synchronizer 是一個對象, 它根據本身的狀態調節線程的控制流程. 主要類型有:
- 訊號量(semaphore)
- 關卡(barrier)
- 閉鎖(latch)
他們的特性: 封裝狀態, 這些狀態絕對著線程執行到某一點時是通過還是被迫等待.
閉鎖 latch
直到 閉鎖 到達 終點狀態 之前, 門一直是關閉的, 沒有線程能夠通過, 在 終點狀態 到來的時候, 門開了, 允許所有線程通過. 一旦到了終點狀態, 他就 不能 再改變狀態了.
用例:
- 確保一個計算不會執行, 直到它需要的資源初始化.
- 確保一個服務不會開始, 直到它依賴的其他服務都已經開始.
- 所有玩家等待就緒, 再開始.
FutureTask
FutureTask 描述了一個抽象的可攜帶結果的計算. FutureTask的計算通過 Callable 實現.
Callable 等價於一個可攜帶結果的 Runnable. Callable 有三種狀態:
- 等待
- 運行
- 完成(包括正常結束, 取消 和 異常)
要擷取 FutureTask 的結果, 可以調用 get() 方法. 調用 get() 時, 有兩種情況:
- 若已經完成, 則直接擷取結果
- 若還未完成, 則阻塞, 直至任務完成返回結果或者拋出異常.
FutureTask 保證了計算結果將計算安全執行緒的傳遞到當前線程.
假如FutureTask執行的任務有異常拋出, 則異常會被封裝在 ExecutionException 裡. 以下代碼可以從 ExecutionException 中取出異常:
try { futureTask.get(); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceOf XXXException) { // 自己想要捕獲的異常 } else { throw launderThrowable(cause); } } public static RuntimeException launderThrowable(Throwable cause) { if (t instanceOf RuntimeException) { return (RuntimeException)t; } else if (t instanceOf Error) { } else { throw new IllegalStateException("Not unchecked", t); } }
訊號量 (Semaphore)
計數訊號量用來控制能夠同時訪問某種資源的活動的數量, 或者同時執行某一操作的數量.
使用計數訊號量之前需要先構造一個, 構造時可以將許可集(permit)總數傳遞進去. 在使用計數訊號量時, 要先嘗試擷取(acquire)一個許可, 假如此時有剩餘許可則繼續執行, 若沒有, 則 阻塞. 使用完之後, 要手動釋放(release)一個許可.
用處:
- 構造一個定長的池.
- 構建有界阻塞容器.
關卡 (CyclicBarrier)
關卡用來阻塞一組線程, 直到 所有線程 達到一個條件. 就像一些家庭成員指定商場的一個集合地點:"我們每個人6:00在麥當勞見, 到了以後不見不散, 之後我們再決定接下來做什麼".
關卡 與 閉鎖 的不同:
關卡: 等待的是其他線程, 可以重複被使用 閉鎖: 等待的是事件, 只能使用一次
當一個線程到達關卡點時, 調用 await, await 會被阻塞, 直到所有線程都到達關卡點.
如果成功地通過關卡, await 為每一個線程返回一個唯一的到達索引號, 可以用來 "選舉" 產生一個領導, 在下一次迭代中承擔一些特殊工作.
CyclicBarrier 也允許你向建構函式傳遞一個 關卡行為(Barrier action), 這是一個 Runnable, 當成功通過關卡的時候, 會(在 某一個 子任務線程中) 執行, 但是在阻塞線程被釋放之前是不能執行的.
Exchanger
Exchanger 是關卡的另一種形式, 它是一種兩步關卡, 在關卡點會交換資料.
Java 並發編程 - 3