java核心技術卷I-線程安全(一)

線程安全的集合

如果多線程要併發地修改一個數據結構, 例如散列表, 那麼很容易會破壞這個數據結構
(有關散列表的詳細信息見第 9 章) 。例如, 一個線程可能要開始向表中插入一個新元素。假定在調整散列表各個桶之間的鏈接關係的過程中, 被剝奪了控制權。如果另一個線程也開始遍歷同一個鏈表,可能使用無效的鏈接並造成混亂, 會拋出異常或者陷人死循環。
可以通過提供鎖來保護共享數據結構, 但是選擇線程安全的實現作爲替代可能更容易些。當然,前一節討論的阻塞隊列就是線程安全的集合。

高效的映射、集和隊列

java.util.concurrent 包提供了映射、 有序集和隊列的高效實現:ConcurrentHashMap、ConcurrentSkipListMap > ConcurrentSkipListSet 和 ConcurrentLinkedQueue。
這些集合使用複雜的算法,通過允許併發地訪問數據結構的不同部分來使競爭極小化。與大多數集合不同,size 方法不必在常量時間內操作。確定這樣的集合當前的大小通常需要遍歷。
有些應用使用龐大的併發散列映射,這些映射太過龐大, 以至於無法用 size 方法得到它的大小, 因爲這個方法只能返回 int。對於一個包含超過 20 億條目的映射該如何處理? JavaSE 8 引入了一個 mappingCount 方法可以把大小作爲 long 返回。
集合返回弱一致性( weakly consistent) 的迭代器。這意味着迭代器不一定能反映出它們被構造之後的所有的修改,但是,它們不會將同一個值返回兩次,也不會拋出 ConcurrentModificationException 異常。與之形成對照的是, 集合如果在迭代器構造之後發生改變,java.util 包中的迭代器將拋出一個 ConcurrentModificationException 異常。
併發的散列映射表, 可高效地支持大量的讀者和一定數量的寫者。默認情況下,假定可以有多達 16 個寫者線程同時執行。可以有更多的寫者線程,但是, 如果同一時間多於 16個,其他線程將暫時被阻塞。可以指定更大數目的構造器,然而, 恐怕沒有這種必要。
在 JavaSE 8 中,併發散列映射將桶組織爲樹, 而不是列表, 鍵類型實現了 Comparable, 從而可以保證性能爲 o(log(n))

映射條目的原子更新

ConcurrentHashMap 原來的版本只有爲數不多的方法可以實現原子更新。
假設多個線程會遇到單詞,我們想統計它們的頻率,下面的代碼不是線程安全的

 Long oldValue = map.get(word);
    Long newValue = oldValue == null ? 1: oldValue + 1;
    map.put(word, newValue); // Error-might not replace oldValue

可能會有另一個線程在同時更新同一個計數。
如果多個線程修改一個普通的 HashMap,它們會破壞內部結構 (一個鏈表數組)。有些鏈接可能丟失, 或者甚至會構成循環,使得這個數據結構不再可用。對於ConcurrentHashMap 絕對不會發生這種情況。在上面的例子中,get 和 put 代碼不會破壞數據結構。不過,由於操作序列不是原子的,所以結果不可預知。
傳統的做法是使用 replace 操作, 它會以原子方式用一個新值替換原值,前提是之前沒有其他線程把原值替換爲其他值。必須一直這麼做, 直到 replace 成功:

do
{
	oldValue = map.get(word);
	newValue = oldValue = null ? 1 : oldValue + 1;
} while (!nap.replace(word, oldValue, newValue));

或者, 可以使用一個 ConcurrentHashMap<String,AtomicLong>, 或者在 Java SE 8中,還可以使用 ConcurrentHashMap<String,LongAdder>。更新代碼如下:

map.putlfAbsent(word, new LongAdder());
map.get(word).increment();

第一個語句確保有一個 LongAdder 可以完成原子自增。由於 putlfAbsent 返回映射的的
值(可能是原來的值, 或者是新設置的值,) 所以可以組合這兩個語句:

map.putlfAbsent(word, new LongAdder()).increraent();

Java SE 8 提供了一些可以更方便地完成原子更新的方法。調用 compute 方法時可以提供一個鍵和一個計算新值的函數。這個函數接收鍵和相關聯的值(如果沒有值,則爲 mill), 它會計算新值。例如,可以如下更新一個整數計數器的映射:

map.compute(word , (k, v) -> v = null ? 1: v + 1);

ConcurrentHashMap 中不允許有 null 值。有很多方法都使用 null 值來指示映射中某個給定的鍵不存在。
另外還有 computelfPresent 和 computelf bsent 方法,它們分別只在已經有原值的情況下計算新值,或者只有沒有原值的情況下計算新值。可以如下更新一個 LongAdder 計數器映射:

map.computelfAbsent(word , k -> new LongAdder()).increment() ;

這與之前看到的 putlfAbsent 調用幾乎是一樣的,不過 LongAdder 構造器只在確實需要一個新的計數器時纔會調用。
首次增加一個鍵時通常需要做些特殊的處理。利用 merge 方法可以非常方便地做到這一點。這個方法有一個參數表示鍵不存在時使用的初始值。否則, 就會調用你提供的函數來結合原值與初始值。(與 compute 不同,這個函數不處理鍵)

map.merge(word, 1L, (existingValue, newValue) -> existingValue + newValue);

或者,更簡單地可以寫爲:

map.merge(word, 1L, Long::sum);

如果傳入 compute 或 merge 的函數返回 null, 將從映射中刪除現有的條目。

對併發散列映射的批操作

Java SE 8 爲併發散列映射提供了批操作,即使有其他線程在處理映射,這些操作也能安全地執行。批操作會遍歷映射,處理遍歷過程中找到的元素。無須凍結當前映射的照。除非你恰好知道批操作運行時映射不會被修改, 否則就要把結果看作是映射狀態的一個近似。
有 3 種不同的操作:

搜索(search) 爲每個鍵或值提供一個函數,直到函數生成一個非 null 的結果。然後搜 索終止,返回這個函數的結果。
歸約(reduce) 組合所有鍵或值, 這裏要使用所提供的一個累加函數。
forEach 爲所有鍵或值提供一個函數

每個操作都有 4 個版本:

operationKeys: 處理鍵。
operatioriValues: 處理值。
operation: 處理鍵和值。
operatioriEntries: 處理 Map.Entry 對象。

對於上述各個操作, 需要指定一個參數化閾值。如果映射包含的元素多於這個閾值, 就會並行完成批操作。如果希望批操作在一個線程中運行,可以使用閾值Long.MAX_VALUE。如果希望用儘可能多的線程運行批操作,可以使用閾值 1。

U searchKeys(long threshold, BiFunction<? super K , ? extends U> f)
U searchValues(long threshold, BiFunction<? super V, ? extends U> f)
U search(long threshold, BiFunction<? super K, ? super V,? extends U> f)
U searchEntries(long threshold, BiFunction<Map.Entry<K, V>, ? extends U> f)

例如, 假設我們希望找出第一個出現次數超過 1000 次的單詞。需要搜索鍵和值:

String result = map.search(threshold, (k, v) -> v > 1000 ? k : null);

result 會設置爲第一個匹配的單詞,如果搜索函數對所有輸人都返回 null, 則返回null。
forEach方法有兩種形式。第一個只爲各個映射條目提供一個消費者函數, 例如:

map.forEach(threshold,(k, v) -> System.out.println(k + " -> " + v));

第二種形式還有一個轉換器函數, 這個函數要先提供, 其結果會傳遞到消費者:

map.forEach(threshold,
(k, v)-> k + " -> " + v
,// Transformer
System.out::println); // Consumer

轉換器可以用作爲一個過濾器。只要轉換器返回 null , 這個值就會被悄無聲息地跳過。
reduce 操作用一個累加函數組合其輸入。例如,可以如下計算所有值的總和:

Long sum = map.reduceValues(threshold, Long::sum);

與 forEach 類似,也可以提供一個轉換器函數。可以如下計算最長的鍵的長度:

Integer maxlength = map.reduceKeys(threshold,
String::length, // Transformer
Integer::max); // Accumulator

轉換器可以作爲一個過濾器,通過返回 null 來排除不想要的輸入。

Long count = map. reduceValues(threshold,
v -> v > 1000 ? 1L : null ,
Long::sum);

如果映射爲空, 或者所有條目都被過濾掉, reduce 操作會返回 null。如果只有一個元素, 則返回其轉換結果, 不會應用累加器。
對於 int、 long 和 double 輸出還有相應的特殊化操作, 分別有後綴 Tolnt、 ToLong 和ToDouble。需要把輸入轉換爲一個基本類型值,並指定一個默認值和一個累加器函數。映射爲空時返回默認值。

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