Java Review - 并发组件ConcurrentHashMap使用时的注意事项及源码分析
文章目錄
- 概述
- 案例
- 原因分析
- 修復
- 小結
概述
ConcurrentHashMap雖然為并發安全的組件,但是使用不當仍然會導致程序錯誤。我們這里通過一個簡單的案例來復現這些問題,并給出開發時如何避免的策略。
案例
來個簡單的例子,比如有幾個注冊中心 , 客戶端要注冊
import com.alibaba.fastjson.JSON;import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap;/*** @author 小工匠* @version 1.0* @description: TODO* @date 2021/11/21 10:46* @mark: show me the code , change the world*/ public class ConcurrentHashMapTest {// 1 創建Map , key為注冊中心地址,value為客戶端列表private static ConcurrentHashMap<String, List<String>> registMap = new ConcurrentHashMap<>();private static final String REGIST_SERVER_A = "注冊中心A";private static final String REGIST_SERVER_B = "注冊中心B";public static void main(String[] args) {// 2 注冊 REGIST_SERVER_AThread threadOne =new Thread(()->{List<String> list = new ArrayList<>();list.add("客戶端一");list.add("客戶端二");registMap.put(REGIST_SERVER_A, list);System.out.println( "注冊信息:" + JSON.toJSONString(registMap));});// 3 注冊 REGIST_SERVER_AThread threadTwo =new Thread(()->{List<String> list = new ArrayList<>();list.add("客戶端三");list.add("客戶端四");registMap.put(REGIST_SERVER_A, list);System.out.println( "注冊信息:" + JSON.toJSONString(registMap));});// 4 注冊 REGIST_SERVER_BThread threadThree =new Thread(()->{List<String> list = new ArrayList<>();list.add("客戶端五");list.add("客戶端六");registMap.put(REGIST_SERVER_B, list);System.out.println("注冊信息:" + JSON.toJSONString(registMap));});// 5 啟動注冊threadOne.start();threadTwo.start();threadThree.start();}}代碼(1)創建了一個并發map,用來存放冊中心地址及與其對應的客戶端列表。
代碼(2)和代碼(3)模擬客戶端注冊REGIST_SERVER_A,代碼(4)模擬客戶端注冊REGIST_SERVER_B。
代碼(5)啟動線程。
運行代碼,輸出結果如下
或者
原因分析
可見,REGIST_SERVER_A中的客戶端會丟失一部分,這是因為put方法如果發現map里面存在這個key,則使用value覆蓋該key對應的老的value值。
/*** Maps the specified key to the specified value in this table.* Neither the key nor the value can be null.** <p>The value can be retrieved by calling the {@code get} method* with a key that is equal to the original key.** @param key key with which the specified value is to be associated* @param value value to be associated with the specified key* @return the previous value associated with {@code key}, or* {@code null} if there was no mapping for {@code key}* @throws NullPointerException if the specified key or value is null*/public V put(K key, V value) {return putVal(key, value, false);}/** Implementation for put and putIfAbsent */final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();int hash = spread(key.hashCode());int binCount = 0;for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;if (tab == null || (n = tab.length) == 0)tab = initTable();else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break; // no lock when adding to empty bin}else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);else {V oldVal = null;synchronized (f) {if (tabAt(tab, i) == f) {if (fh >= 0) {binCount = 1;for (Node<K,V> e = f;; ++binCount) {K ek;if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {oldVal = e.val;if (!onlyIfAbsent)e.val = value;break;}Node<K,V> pred = e;if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key,value, null);break;}}}else if (f instanceof TreeBin) {Node<K,V> p;binCount = 2;if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}if (binCount != 0) {if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}addCount(1L, binCount);return null;}而putIfAbsent方法則是,如果發現已經存在該key則返回該key對應的value,但并不進行覆蓋,如果不存在則新增該key,并且判斷和寫入是原子性操作。
/*** {@inheritDoc}** @return the previous value associated with the specified key,* or {@code null} if there was no mapping for the key* @throws NullPointerException if the specified key or value is null*/public V putIfAbsent(K key, V value) {return putVal(key, value, true);}第三個參數 putIfAbsent為true。
修復
使用putIfAbsent替代put方法后的代碼如下。
import com.alibaba.fastjson.JSON; import org.springframework.util.CollectionUtils;import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap;/*** @author 小工匠* @version 1.0* @description: TODO* @date 2021/11/21 10:46* @mark: show me the code , change the world*/ public class ConcurrentHashMapTest2 {// 1 創建Map , key為注冊中心地址,value為客戶端列表private static ConcurrentHashMap<String, List<String>> registMap = new ConcurrentHashMap<>();private static final String REGIST_SERVER_A = "注冊中心A";private static final String REGIST_SERVER_B = "注冊中心B";public static void main(String[] args) {// 2 注冊 REGIST_SERVER_AThread threadOne =new Thread(()->{List<String> list = new ArrayList<>();list.add("客戶端一");list.add("客戶端二");// 若果原集合不為空,則追加新的集合List<String> oldList = registMap.putIfAbsent(REGIST_SERVER_A, list);if (null != oldList){oldList.addAll(list);}System.out.println( "注冊信息:" + JSON.toJSONString(registMap));});// 3 注冊 REGIST_SERVER_AThread threadTwo =new Thread(()->{List<String> list = new ArrayList<>();list.add("客戶端三");list.add("客戶端四");List<String> oldList = registMap.putIfAbsent(REGIST_SERVER_A, list);// 若果原集合不為空,則追加新的集合if (!CollectionUtils.isEmpty(oldList)){oldList.addAll(list);}System.out.println( "注冊信息:" + JSON.toJSONString(registMap));});// 4 注冊 REGIST_SERVER_BThread threadThree =new Thread(()->{List<String> list = new ArrayList<>();list.add("客戶端五");list.add("客戶端六");List<String> oldList = registMap.putIfAbsent(REGIST_SERVER_B, list);if (!CollectionUtils.isEmpty(oldList)){oldList.addAll(list);}System.out.println("注冊信息:" + JSON.toJSONString(registMap));});// 5 啟動注冊threadOne.start();threadTwo.start();threadThree.start();}}使用map.putIfAbsent方法添加新終端列表,如果REGIST_SERVER_A在map中不存在,則將REGIST_SERVER_A和對應終端列表放入map。
要注意的是,這個判斷和放入是原子性操作,放入后會返回null。如果REGIST_SERVER_A已經在map里面存在,則調用putIfAbsent會返回REGIST_SERVER_A對應的終端列表,若發現返回的終端列表不為null則把新的終端列表添加到返回的設備列表里面,從而問題得到解決。
小結
-
put(K key, V value) 方法判斷如果key已經存在,則使用value覆蓋原來的值并返回原來的值,如果不存在則把value放入并返回null。
-
而putIfAbsent(K key, V value)方法則是如果key已經存在則直接返回原來對應的值并不使用value覆蓋,如果key不存在則放入value并返回null,
-
另外要注意,判斷key是否存在和放入是原子性操作。
總結
以上是生活随笔為你收集整理的Java Review - 并发组件ConcurrentHashMap使用时的注意事项及源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java Review - 线程池中使用
- 下一篇: Java Review - Simple