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, 线程 one
Thread 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,线程 two
Thread 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,线程 three
Thread 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, 线程 one
Thread 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,线程 two
Thread 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,线程 three
Thread 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", "devi
ce222"]}
{"topic1":["device1", "device2", "device11", "device22"], "topic2":["device111", "devi
ce222"]}
{"topic1":["device1", "device2", "device11", "device22"], "topic2":["device111", "devi
ce222"]}
总结:put(K key, V value)方法判断如果 key 已经存在,则使用 value 覆盖原来的值并返回原来的值,如果不存在则把 value 放入并返回 null。而 putIfAbsent(K key, V value)方法则是如果 key 已经存在则直接返回原来对应的值并不使用 value 覆盖,如果 key 不存在则放入 value 并返回 null,另外要注意,判断 key 是否存在和放入是原子性操作。