Java Review - 并发组件ConcurrentHashMap使用时的注意事项及源码分析
文章目錄
- 概述
- 案例
- 原因分析
- 修復(fù)
- 小結(jié)
概述
ConcurrentHashMap雖然為并發(fā)安全的組件,但是使用不當(dāng)仍然會(huì)導(dǎo)致程序錯(cuò)誤。我們這里通過一個(gè)簡(jiǎn)單的案例來(lái)復(fù)現(xiàn)這些問題,并給出開發(fā)時(shí)如何避免的策略。
案例
來(lái)個(gè)簡(jiǎn)單的例子,比如有幾個(gè)注冊(cè)中心 , 客戶端要注冊(cè)
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 創(chuàng)建Map , key為注冊(cè)中心地址,value為客戶端列表private static ConcurrentHashMap<String, List<String>> registMap = new ConcurrentHashMap<>();private static final String REGIST_SERVER_A = "注冊(cè)中心A";private static final String REGIST_SERVER_B = "注冊(cè)中心B";public static void main(String[] args) {// 2 注冊(cè) REGIST_SERVER_AThread threadOne =new Thread(()->{List<String> list = new ArrayList<>();list.add("客戶端一");list.add("客戶端二");registMap.put(REGIST_SERVER_A, list);System.out.println( "注冊(cè)信息:" + JSON.toJSONString(registMap));});// 3 注冊(cè) REGIST_SERVER_AThread threadTwo =new Thread(()->{List<String> list = new ArrayList<>();list.add("客戶端三");list.add("客戶端四");registMap.put(REGIST_SERVER_A, list);System.out.println( "注冊(cè)信息:" + JSON.toJSONString(registMap));});// 4 注冊(cè) REGIST_SERVER_BThread threadThree =new Thread(()->{List<String> list = new ArrayList<>();list.add("客戶端五");list.add("客戶端六");registMap.put(REGIST_SERVER_B, list);System.out.println("注冊(cè)信息:" + JSON.toJSONString(registMap));});// 5 啟動(dòng)注冊(cè)threadOne.start();threadTwo.start();threadThree.start();}}代碼(1)創(chuàng)建了一個(gè)并發(fā)map,用來(lái)存放冊(cè)中心地址及與其對(duì)應(yīng)的客戶端列表。
代碼(2)和代碼(3)模擬客戶端注冊(cè)REGIST_SERVER_A,代碼(4)模擬客戶端注冊(cè)REGIST_SERVER_B。
代碼(5)啟動(dòng)線程。
運(yùn)行代碼,輸出結(jié)果如下
或者
原因分析
可見,REGIST_SERVER_A中的客戶端會(huì)丟失一部分,這是因?yàn)閜ut方法如果發(fā)現(xiàn)map里面存在這個(gè)key,則使用value覆蓋該key對(duì)應(yīng)的老的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方法則是,如果發(fā)現(xiàn)已經(jīng)存在該key則返回該key對(duì)應(yīng)的value,但并不進(jìn)行覆蓋,如果不存在則新增該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);}第三個(gè)參數(shù) putIfAbsent為true。
修復(fù)
使用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 創(chuàng)建Map , key為注冊(cè)中心地址,value為客戶端列表private static ConcurrentHashMap<String, List<String>> registMap = new ConcurrentHashMap<>();private static final String REGIST_SERVER_A = "注冊(cè)中心A";private static final String REGIST_SERVER_B = "注冊(cè)中心B";public static void main(String[] args) {// 2 注冊(cè) 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( "注冊(cè)信息:" + JSON.toJSONString(registMap));});// 3 注冊(cè) 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( "注冊(cè)信息:" + JSON.toJSONString(registMap));});// 4 注冊(cè) 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("注冊(cè)信息:" + JSON.toJSONString(registMap));});// 5 啟動(dòng)注冊(cè)threadOne.start();threadTwo.start();threadThree.start();}}使用map.putIfAbsent方法添加新終端列表,如果REGIST_SERVER_A在map中不存在,則將REGIST_SERVER_A和對(duì)應(yīng)終端列表放入map。
要注意的是,這個(gè)判斷和放入是原子性操作,放入后會(huì)返回null。如果REGIST_SERVER_A已經(jīng)在map里面存在,則調(diào)用putIfAbsent會(huì)返回REGIST_SERVER_A對(duì)應(yīng)的終端列表,若發(fā)現(xiàn)返回的終端列表不為null則把新的終端列表添加到返回的設(shè)備列表里面,從而問題得到解決。
小結(jié)
-
put(K key, V value) 方法判斷如果key已經(jīng)存在,則使用value覆蓋原來(lái)的值并返回原來(lái)的值,如果不存在則把value放入并返回null。
-
而putIfAbsent(K key, V value)方法則是如果key已經(jīng)存在則直接返回原來(lái)對(duì)應(yīng)的值并不使用value覆蓋,如果key不存在則放入value并返回null,
-
另外要注意,判斷key是否存在和放入是原子性操作。
總結(jié)
以上是生活随笔為你收集整理的Java Review - 并发组件ConcurrentHashMap使用时的注意事项及源码分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java Review - 线程池中使用
- 下一篇: Java Review - Simple