Java 並發編程 - 3

來源:互聯網
上載者:User

標籤:代碼   結束   timeout   string   同步   expected   null   something   mit   

 JDK 1.5 之前的同步容器

JDK 1.5 之前, 主要包括:

  • 同步容器 (Vector 和 Hashtable)
  • 同步封裝類 (Collections.synchronizedXxx)

這些類的共同特徵是, 公用方法都是由 synchronized 來修飾的, 以限制一次只能有一個線程能訪問容器.

同步容器中出現的問題複合操作

老的容器自身並不支援複合操作, 包括:

  1. 迭代(反覆擷取元素, 直到獲得容器中的最後一個元素)
  2. 導航(navigation, 根據一定的順序尋找下一個元素)
  3. 條件運算(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 方法

隱藏的迭代器

有時候, 一些操作會隱含的調用迭代器, 比如:

  1. 調用 toString() 方法, 尤其是寫 log 時, 有 

    log("Set:" + set);

    這樣的語句.

  2. 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;}
  3. 另外 containAllremoveAll 和 retainAll 也會產生迭代.

JDK 1.5 之後的容器

JDK 1.5 後, 新增加了:

  • ConcurrentHashMap, 來替代同步的 Map 實現, 增加了 put-if-absent, 替換和條件刪除
  • CopyOnWriteArrayList, 是 List 相應的同步實現
  • Queue, 用來臨時儲存正在等待進一步處理的一系列元素, 實現包括
    • ConcurrentLinkedQueue, 一個傳統的 FIFO 隊列
    • PriorityQueue, 一個(非並發)居右優先順序順序的隊列
  • BlockingQueue, 拓展自 Queue, 增加了可阻塞的插入和擷取操作. 
    • 如果隊列是空的, 那麼擷取操作會被阻塞直到有元素存在; 
    • 如果隊列是滿的, 那麼插入操作會被阻塞直到有有元素被取出.

JDK 1.6 後, 新增加了 

  1. Deque 和 BlockingDeque, 分別擴充了 Queue 和 BlockingQueue:

    • Deque 介面, 實作類別是 ArrayDeque, 不阻塞
    • BlockingDeque 介面, 實作類別是 LinkedBlockingDeque, 阻塞.
  2. ConcurrentSkipListMap 和 ConcurrentSkipListSet, 作為 SortedMap 和 SortedSet 的並發替代品

Note: 從一個空的Queue中取元素, 並不會阻塞, 而是返回 null

ConcurrentHashMap

在 ConcurrentHashMap 之前, HashTable 和 SynchronizedMap 都是通過給整個方法加 synchronized 來達到同步的, 這樣限制某一時刻只有一個線程可以訪問容器.

ConcurrentHashMap 使用一個更加細化的鎖機制, 名叫分離鎖. 這個機制允許更深層次的共用訪問: 

  • 任意數量的讀線程可以並發訪問 Map.
  • 讀者和寫者可以並發訪問 Map.
  • 有限數量的寫線程可以並發修改 Map.

由於並發環境中, Map 的大小通常是動態, size 和 isEmpty 返回的只是個估算值(可能返回後接著到期).

支援的複合操作:

  1. put-if-absent
  2. remove-if-equal
  3. 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 方法. 常見的實現有:

  1. LinkedBlockedQueue, FIFO, 鏈表實現, 隊列首 take, 隊列尾 put.
  2. ArrayBlockingQueue, FIFO, 數組實現, 可以在 putIndex(隊列尾) 插入, 從 takeIndex(隊列首) 取出.
  3. PriorityBlockingQueue, 根據 Comparator 排序次序取出
  4. SynchronousQueue, 生產線程直接和消費線程對接, 如果生產線程找不到消費者或反之, 則, put 和 take 會一直阻止. 只有在消費者充足的時候比較適合, 他們總能為下一個任務做好準備.
雙端隊列和竊取工作

雙端隊列用來實現 竊取工作(work stealing) 模式. 

在傳統的 生產者-消費者 設計中, 所有的消費者只共用一個工作隊列.

而在 竊取工作 設計中, 每一個消費者都有一個自己的雙端隊列. 如果一個消費者完成了自己雙端隊列中的全部工作, 它可以偷取其他消費者的雙端隊列的 末尾 任務(其他消費者仍然從隊列  取任務). 

因為工作者線程並不會競爭一個共用的任務隊列, 所以 竊取工作 模式比傳統的 生產者-消費者 設計有更好的伸縮性.

阻塞和可中斷的方法

阻塞: 線程被掛起, 狀態變為BLOCKEDWAITING 或是 TIMED_WAITING等待直到一個事件發生才能繼續進行.

BlockingQueue 的 put 和 take 方法會拋出一個受檢查的 InterruptedException, 這個異常說明這是個阻塞方法, 可以被中斷來提前結束阻塞.

處理中斷的方法:

  • 傳遞 InterruptedException. 傳遞給調用者, 可以對其中特定活動進行簡潔地清理後, 再拋出.
  • 恢複中斷. 當代碼是 Runnable的一部分時, 必須捕獲 InterruptedException. 並且, 在當前線程中調用 interrupt 重新設定中斷狀態(拋出異常會清理中斷標誌位), 這樣調用棧中更高層代碼可以發現中斷已經發生. 

    try {    processTask(queue.take());} catch (InterruptedException e) {    // 恢複中斷狀態    Thread.currentThread().interrupt();}
Synchronizer

Synchronizer 是一個對象, 它根據本身的狀態調節線程的控制流程. 主要類型有:

  1. 訊號量(semaphore)
  2. 關卡(barrier)
  3. 閉鎖(latch)

他們的特性: 封裝狀態, 這些狀態絕對著線程執行到某一點時是通過還是被迫等待.

閉鎖 latch

直到 閉鎖 到達 終點狀態 之前, 門一直是關閉的, 沒有線程能夠通過, 在 終點狀態 到來的時候, 門開了, 允許所有線程通過. 一旦到了終點狀態, 他就 不能 再改變狀態了.

用例:

  1. 確保一個計算不會執行, 直到它需要的資源初始化.
  2. 確保一個服務不會開始, 直到它依賴的其他服務都已經開始.
  3. 所有玩家等待就緒, 再開始.
FutureTask

FutureTask 描述了一個抽象的可攜帶結果的計算. FutureTask的計算通過 Callable 實現.

Callable 等價於一個可攜帶結果的 RunnableCallable 有三種狀態:

  1. 等待
  2. 運行
  3. 完成(包括正常結束, 取消 和 異常)

要擷取 FutureTask 的結果, 可以調用 get() 方法. 調用 get() 時, 有兩種情況:

  1. 若已經完成, 則直接擷取結果
  2. 若還未完成, 則阻塞, 直至任務完成返回結果或者拋出異常.

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)一個許可. 

用處:

  1. 構造一個定長的池.
  2. 構建有界阻塞容器.
關卡 (CyclicBarrier)

關卡用來阻塞一組線程, 直到 所有線程 達到一個條件. 就像一些家庭成員指定商場的一個集合地點:"我們每個人6:00在麥當勞見, 到了以後不見不散, 之後我們再決定接下來做什麼". 

關卡 與 閉鎖 的不同:

關卡: 等待的是其他線程, 可以重複被使用 閉鎖: 等待的是事件, 只能使用一次

當一個線程到達關卡點時, 調用 awaitawait 會被阻塞, 直到所有線程都到達關卡點.

  • 如果所有線程都到達了關卡點, 關卡就被成功地突破, 所有線程會被釋放.
  • 如果對 await 的調用逾時, 或者阻塞中的線程被中斷, 那麼關卡就被認為是 失敗 的. 

    • 若某一個線程調用有時限的 await, 那麼當這個線程 await 逾時, 這個線程會拋出 TimeoutException 異常, 其他調用 barrior.await() 的線程會拋出 BrokenBarrierException;

如果成功地通過關卡, await 為每一個線程返回一個唯一的到達索引號, 可以用來 "選舉" 產生一個領導, 在下一次迭代中承擔一些特殊工作.

CyclicBarrier 也允許你向建構函式傳遞一個 關卡行為(Barrier action), 這是一個 Runnable, 當成功通過關卡的時候, 會(在 某一個 子任務線程中) 執行, 但是在阻塞線程被釋放之前是不能執行的.

Exchanger

Exchanger 是關卡的另一種形式, 它是一種兩步關卡, 在關卡點會交換資料.

Java 並發編程 - 3

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.