ConcurrentHashMap 虽然为并发安全的组件,但是使用不当仍然会导致程序错误。本节通过简单的案例来复现这些问题,并给出开发时如何避免的策略。

    这里借用直播的一个场景,在直播业务中,每个直播间对应一个 topic,每个用户进入直播间时会把自己设备的 ID 绑定到这个 topic 上,也就是一个 topic 对应一堆用户设备。可以使用 map 来维护这些信息,其中 key 为 topic,value 为设备的 list。下面使用代码来模拟多用户同时进入直播间时 map 信息的维护。

    1. public class TestMap {
    2. //(1)创建 map, key 为 topic, value 为设备列表
    3. static ConcurrentHashMap<String List<String>> map = new ConcurrentHashMap<>();
    4. public static void mainString[] args {
    5. //(2)进入直播间 topic1, 线程 one
    6. Thread threadOne = new Thread(new Runnable() {
    7. public void run() {
    8. List<String> list1 = new ArrayList<>();
    9. list1.add(「device1」);
    10. list1.add(「device2」);
    11. map.put(「topic1」, list1);
    12. System.out.println(JSON.toJSONStringmap));
    13. }
    14. });
    15. //(3)进入直播间 topic1,线程 two
    16. Thread threadTwo = new Thread(new Runnable() {
    17. public void run() {
    18. List<String> list1 = new ArrayList<>();
    19. list1.add(「device11」);
    20. list1.add(「device22」);
    21. map.put(「topic1」, list1);
    22. System.out.println(JSON.toJSONStringmap));
    23. }
    24. });
    25. //(4)进入直播间 topic2,线程 three
    26. Thread threadThree = new Thread(new Runnable() {
    27. public void run() {
    28. List<String> list1 = new ArrayList<>();
    29. list1.add(「device111」);
    30. list1.add(「device222」);
    31. map.put(「topic2」, list1);
    32. System.out.printlnJSON.toJSONString(map));
    33. }
    34. });
    35. //(5)启动线程
    36. threadOne.start();
    37. threadTwo.start();
    38. threadThree.start();
    39. }
    40. }

    代码(1)创建了一个并发 map,用来存放 topic 及与其对应的设备列表。

    代码(2)和代码(3)模拟用户进入直播间 topic1,代码(4)模拟用户进入直播间 topic2。

    代码(5)启动线程。

    运行代码,输出结果如下。

    1. {"topic1":["device11", "device22"], "topic2":["device111", "device222"]}
    2. {"topic1":["device11", "device22"], "topic2":["device111", "device222"]}
    3. {"topic1":["device11", "device22"], "topic2":["device111", "device222"]}

    或者输出如下。

    1. {"topic1":["device1", "device2"], "topic2":["device111", "device222"]}
    2. {"topic1":["device1", "device2"], "topic2":["device111", "device222"]}
    3. {"topic1":["device1", "device2"], "topic2":["device111", "device222"]}

    可见,topic1 房间中的用户会丢失一部分,这是因为 put 方法如果发现 map 里面存在这个 key,则使用 value 覆盖该 key 对应的老的 value 值。而 putIfAbsent 方法则是,如果发现已经存在该 key 则返回该 key 对应的 value,但并不进行覆盖,如果不存在则新增该 key,并且判断和写入是原子性操作。使用 putIfAbsent 替代 put 方法后的代码如下。

    1. public class TestMap2 {
    2. //(1)创建 map, key 为 topic, value 为设备列表
    3. static ConcurrentHashMap<String List<String>> map = new ConcurrentHashMap<>();
    4. public static void mainString[] args {
    5. //(2)进入直播间 topic1, 线程 one
    6. Thread threadOne = new Thread(new Runnable() {
    7. public void run() {
    8. List<String> list1 = new ArrayList<>();
    9. list1.add(「device1」);
    10. list1.add(「device2」);
    11. //(2.1)
    12. List<String> oldList = map.putIfAbsent(「topic1」, list1);
    13. ifnull = oldList){
    14. oldList.addAlllist1);
    15. }
    16. System.out.println(JSON.toJSONStringmap));
    17. }
    18. });
    19. //(3)进入直播间 topic1,线程 two
    20. Thread threadTwo = new Thread(new Runnable() {
    21. public void run() {
    22. List<String> list1 = new ArrayList<>();
    23. list1.add(「device11」);
    24. list1.add(「device22」);
    25. List<String> oldList = map.putIfAbsent(「topic1」, list1);
    26. ifnull = oldList){
    27. oldList.addAlllist1);
    28. }
    29. System.out.println(JSON.toJSONStringmap));
    30. }
    31. });
    32. //(4)进入直播间 topic2,线程 three
    33. Thread threadThree = new Thread(new Runnable() {
    34. public void run() {
    35. List<String> list1 = new ArrayList<>();
    36. list1.add(「device111」);
    37. list1.add(「device222」);
    38. List<String> oldList = map.putIfAbsent(「topic2」, list1);
    39. ifnull = oldList){
    40. oldList.addAlllist1);
    41. }
    42. System.out.println(JSON.toJSONStringmap));
    43. }
    44. });
    45. //(5)启动线程
    46. threadOne.start();
    47. threadTwo.start();
    48. threadThree.start();
    49. }
    50. }

    在如上代码(2.1)中,使用 map.putIfAbsent 方法添加新设备列表,如果 topic1 在 map 中不存在,则将 topic1 和对应设备列表放入 map。要注意的是,这个判断和放入是原子性操作,放入后会返回 null。如果 topic1 已经在 map 里面存在,则调用 putIfAbsent 会返回 topic1 对应的设备列表,若发现返回的设备列表不为 null 则把新的设备列表添加到返回的设备列表里面,从而问题得到解决。

    运行结果为

    1. {"topic1":["device1", "device2", "device11", "device22"], "topic2":["device111", "devi
    2. ce222"]}
    3. {"topic1":["device1", "device2", "device11", "device22"], "topic2":["device111", "devi
    4. ce222"]}
    5. {"topic1":["device1", "device2", "device11", "device22"], "topic2":["device111", "devi
    6. ce222"]}

    总结:put(K key, V value)方法判断如果 key 已经存在,则使用 value 覆盖原来的值并返回原来的值,如果不存在则把 value 放入并返回 null。而 putIfAbsent(K key, V value)方法则是如果 key 已经存在则直接返回原来对应的值并不使用 value 覆盖,如果 key 不存在则放入 value 并返回 null,另外要注意,判断 key 是否存在和放入是原子性操作。