1. 程式人生 > >JAVA 8 併發增強 (3)ConcurrentHashMap改進

JAVA 8 併發增強 (3)ConcurrentHashMap改進

/**
 * ConcurrentHashMap是線性安全的,多個執行緒不需要對內部結構造成破壞,就可以刪除或者新增元素。
 * 效能高,允許多個執行緒併發更新雜湊表的不停部分,而不會造成相互堵塞
 * ConcurrentHashMap的size是int型別,J8為了應付數量巨大的併發雜湊對映,引入了一個mappingCount方法
 * 用來返回一個反應大小的long型值 tips:雜湊對映將會將具有相同雜湊碼的所有資料儲存在同一個“塊”中。某些應用程式使用了糟糕的雜湊函式,
 * 導致所有資料項都被儲存在了很小的一組塊中,這嚴重影響了雜湊對映的效率。即使是一般認為合理的雜湊函式,
 * 例如String類的hashCode方法,也可能會存在問題。例如,攻擊者可以通過構造一組大量的雜湊碼都一樣的字串來拉低程式的速度。
 * 在java8中,併發雜湊對映用樹形結構來組織“塊”,而不再用散列表的結構,這樣當鍵類實現了Comparable介面時, 可以保證效能為a(log(n))
 * tips:看起來一個本應執行緒安全的資料結構竟然允許執行緒不安全的操作。但是這是出於兩種不同的考慮。
 * 如果多個執行緒修改一個普通的HashMap,他們可能會破壞內部結構(一個連結串列陣列)。其中一些連結可能會丟失,或者形成了迴路
 * 從而導致資料結構不可用。在conccurentHashMap中這永遠不會發生。get和put程式碼永遠不會破壞資料結構。
 * 但是由於操作順序不是原子的,因此結果也無法預測。
 */
public class ConcurrentHashMapTest {

	public static void main(String[] args) {
		ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();
		// ①錯誤的方式
		String word = "key";
		Long oldValue = map.get(word);
		Long newValue = oldValue == null ? 1 : oldValue + 1;
		map.put(word, newValue);
		// ②一種補救措施是使用replace操作,將一個已知的舊值替換為一個新值。
		do {
			oldValue = map.get(word);
			newValue = oldValue == null ? 1 : oldValue + 1;
		} while (!map.replace(word, oldValue, newValue));

		/** 改進方法1 */
		Map<String, AtomicLong> map2 = new ConcurrentHashMap<String, AtomicLong>();
		/** 改進方法2,僅限於java8以上 */
		Map<String, LongAdder> map3 = new ConcurrentHashMap<String, LongAdder>();
		map3.put(word, new LongAdder());
		/** 改進方法2.1,將兩條語句合為一條 */
		map3.putIfAbsent(word, new LongAdder()).increment();
		/**
		 * j8提供了很多可以更方便進行原子更新的方法。compute可以通過一個鍵和一個函式來計算出新的值
		 * 該函式會獲取鍵及其所關聯的值(如果沒有值則為null),然後計算出新的值 下面是更新一個整型計數器的對映
		 */
		map.compute(word, (k, v) -> v == null ? 1 : v + 1);
		/**
		 * 此外,ConcurrentHashMap還提供了computeIfPresent和computeIfAbsent方法,
		 * 分別在已經存在值和尚未存在值的情況下,才計算新值。因此LongAdder計算器的對映可以更新為如下程式碼:
		 * 這與之間的putIfAbsent的方法非常相似,但是LongAdder建構函式只有在需要一個 新計數器時才會呼叫
		 */
		map3.computeIfAbsent(word, k -> new LongAdder()).increment();
		/**
		 * 通常,當一個鍵第一次被加入到對映時,你需要做一些特殊的事情,那麼使用merge方法會非常方便
		 * 它的一個引數用來表示鍵尚未存在時的初始值。相反,只有在已有值和初始值相結合的時候,你提供的函式才會被呼叫
		 * (不像compute方法,該函式不會處理鍵)
		 */
		map.merge(word, 1L, (existingVal, newVal) -> existingVal + newVal);
		/**
		 * 或者這樣
		 * tips:①如果傳遞給compute或者merge方法的函式返回null,那麼已有的資料項會從對映表中抹掉
		 * ②當你使用compute或者merge方法時,請牢記你所提供的函式不應該進行大量工作。
		 * 當函式執行時,其他一些更新對映的操作可能會被阻塞,當然該函式也不應該更新對映的其他部分
		 */
		map.merge(word, 1L, Long::sum);
	}