ConcurrentHashMap 虽然为并发安全的组件,但是使用不当仍然会导致程序错误。本节通过简单的案例来复现这些问题,并给出开发时如何避免的策略。
这里借用直播的一个场景,在直播业务中,每个直播间对应一个 topic,每个用户进入直播间时会把自己设备的 ID 绑定到这个 topic 上,也就是一个 topic 对应一堆用户设备。可以使用 map 来维护这些信息,其中 key 为 topic,value 为设备的 list。下面使用代码来模拟多用户同时进入直播间时 map 信息的维护。
public class TestMap {//(1)创建 map, key 为 topic, value 为设备列表static ConcurrentHashMap<String, List<String>> map = new ConcurrentHashMap<>();public static void main(String[] args) {//(2)进入直播间 topic1, 线程 oneThread threadOne = new Thread(new Runnable() {public void run() {List<String> list1 = new ArrayList<>();list1.add(「device1」);list1.add(「device2」);map.put(「topic1」, list1);System.out.println(JSON.toJSONString(map));}});//(3)进入直播间 topic1,线程 twoThread threadTwo = new Thread(new Runnable() {public void run() {List<String> list1 = new ArrayList<>();list1.add(「device11」);list1.add(「device22」);map.put(「topic1」, list1);System.out.println(JSON.toJSONString(map));}});//(4)进入直播间 topic2,线程 threeThread threadThree = new Thread(new Runnable() {public void run() {List<String> list1 = new ArrayList<>();list1.add(「device111」);list1.add(「device222」);map.put(「topic2」, list1);System.out.println(JSON.toJSONString(map));}});//(5)启动线程threadOne.start();threadTwo.start();threadThree.start();}}
代码(1)创建了一个并发 map,用来存放 topic 及与其对应的设备列表。
代码(2)和代码(3)模拟用户进入直播间 topic1,代码(4)模拟用户进入直播间 topic2。
代码(5)启动线程。
运行代码,输出结果如下。
{"topic1":["device11", "device22"], "topic2":["device111", "device222"]}{"topic1":["device11", "device22"], "topic2":["device111", "device222"]}{"topic1":["device11", "device22"], "topic2":["device111", "device222"]}
或者输出如下。
{"topic1":["device1", "device2"], "topic2":["device111", "device222"]}{"topic1":["device1", "device2"], "topic2":["device111", "device222"]}{"topic1":["device1", "device2"], "topic2":["device111", "device222"]}
可见,topic1 房间中的用户会丢失一部分,这是因为 put 方法如果发现 map 里面存在这个 key,则使用 value 覆盖该 key 对应的老的 value 值。而 putIfAbsent 方法则是,如果发现已经存在该 key 则返回该 key 对应的 value,但并不进行覆盖,如果不存在则新增该 key,并且判断和写入是原子性操作。使用 putIfAbsent 替代 put 方法后的代码如下。
public class TestMap2 {//(1)创建 map, key 为 topic, value 为设备列表static ConcurrentHashMap<String, List<String>> map = new ConcurrentHashMap<>();public static void main(String[] args) {//(2)进入直播间 topic1, 线程 oneThread threadOne = new Thread(new Runnable() {public void run() {List<String> list1 = new ArrayList<>();list1.add(「device1」);list1.add(「device2」);//(2.1)List<String> oldList = map.putIfAbsent(「topic1」, list1);if(null ! = oldList){oldList.addAll(list1);}System.out.println(JSON.toJSONString(map));}});//(3)进入直播间 topic1,线程 twoThread threadTwo = new Thread(new Runnable() {public void run() {List<String> list1 = new ArrayList<>();list1.add(「device11」);list1.add(「device22」);List<String> oldList = map.putIfAbsent(「topic1」, list1);if(null ! = oldList){oldList.addAll(list1);}System.out.println(JSON.toJSONString(map));}});//(4)进入直播间 topic2,线程 threeThread threadThree = new Thread(new Runnable() {public void run() {List<String> list1 = new ArrayList<>();list1.add(「device111」);list1.add(「device222」);List<String> oldList = map.putIfAbsent(「topic2」, list1);if(null ! = oldList){oldList.addAll(list1);}System.out.println(JSON.toJSONString(map));}});//(5)启动线程threadOne.start();threadTwo.start();threadThree.start();}}
在如上代码(2.1)中,使用 map.putIfAbsent 方法添加新设备列表,如果 topic1 在 map 中不存在,则将 topic1 和对应设备列表放入 map。要注意的是,这个判断和放入是原子性操作,放入后会返回 null。如果 topic1 已经在 map 里面存在,则调用 putIfAbsent 会返回 topic1 对应的设备列表,若发现返回的设备列表不为 null 则把新的设备列表添加到返回的设备列表里面,从而问题得到解决。
运行结果为
{"topic1":["device1", "device2", "device11", "device22"], "topic2":["device111", "device222"]}{"topic1":["device1", "device2", "device11", "device22"], "topic2":["device111", "device222"]}{"topic1":["device1", "device2", "device11", "device22"], "topic2":["device111", "device222"]}
总结:put(K key, V value)方法判断如果 key 已经存在,则使用 value 覆盖原来的值并返回原来的值,如果不存在则把 value 放入并返回 null。而 putIfAbsent(K key, V value)方法则是如果 key 已经存在则直接返回原来对应的值并不使用 value 覆盖,如果 key 不存在则放入 value 并返回 null,另外要注意,判断 key 是否存在和放入是原子性操作。
