概述
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_A Thread 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_A Thread 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_B Thread 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_A Thread 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_A Thread 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_B Thread 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是否存在和放入是原子性操作。