线程安全的并发工具--ConcurrentHashMap

34 阅读2分钟
  • 使用 ConcurrentHashMap 来统计,Key 的范围是 10。
  • 使用最多 10 个并发,循环操作 1000 万次,每次操作累加随机的 Key。
  • 如果 Key 不存在的话,首次设置值为 1
    1. //循环次数  
  1. private static int LOOP_COUNT = 10000000;  
  2. //线程数量  
  3. private static int THREAD_COUNT = 10;  
  4. //元素数量  
  5. private static int ITEM_COUNT = 10;  
  6. private Map<String, Long> normaluse() throws InterruptedException {  
  7.     ConcurrentHashMap<String, Long> freqs = new ConcurrentHashMap<>(ITEM_COUNT);  
  8.     ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);  
  9.     forkJoinPool.execute(() -> IntStream.rangeClosed(1, LOOP_COUNT).parallel().forEach(i -> {  
  10.         //获得一个随机的Key  
  11.         String key = "item" + ThreadLocalRandom.current().nextInt(ITEM_COUNT);  
  12.                 synchronized (freqs) {        
  13.                     if (freqs.containsKey(key)) {  
  14.                         //Key存在则+1  
  15.                         freqs.put(key, freqs.get(key) + 1);  
  16.                     } else {  
  17.                         //Key不存在则初始化为1  
  18.                         freqs.put(key, 1L);  
  19.                     }  
  20.                 }  
  21.             }  
  22.     ));  
  23.     forkJoinPool.shutdown();  
  24.     forkJoinPool.awaitTermination(1, TimeUnit.HOURS);  
  25.     return freqs;  
  26. }

直接通过锁的方式锁住 Map,然后做判断、读取现在的累计值、加 1、保存累加后值的逻辑。这段代码在功能上没有问题,但无法充分发挥 ConcurrentHashMap 的威力

  1. private Map<String, Long> gooduse() throws InterruptedException {  
  2.     ConcurrentHashMap<String, LongAdder> freqs = new ConcurrentHashMap<>(ITEM_COUNT);  
  3.     ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);  
  4.     forkJoinPool.execute(() -> IntStream.rangeClosed(1, LOOP_COUNT).parallel().forEach(i -> {  
  5.         String key = "item" + ThreadLocalRandom.current().nextInt(ITEM_COUNT);  
  6.                 //利用computeIfAbsent()方法来实例化LongAdder,然后利用LongAdder来进行线程安全计数  
  7.                 freqs.computeIfAbsent(key, k -> new LongAdder()).increment();  
  8.             }  
  9.     ));  
  10.     forkJoinPool.shutdown();  
  11.     forkJoinPool.awaitTermination(1, TimeUnit.HOURS);  
  12.     //因为我们的Value是LongAdder而不是Long,所以需要做一次转换才能返回  
  13.     return freqs.entrySet().stream()  
  14.             .collect(Collectors.toMap(  
  15.                     e -> e.getKey(),  
  16.                     e -> e.getValue().longValue())  
  17.             );  
  18. }

这段改进后的代码中,我们巧妙利用了下面两点:

  • 使用 ConcurrentHashMap 的原子性方法 computeIfAbsent 来做复合逻辑操作,判断 Key 是否存在 Value,如果不存在则把 Lambda 表达式运行后的结果放入 Map 作为 Value,也就是新创建一个 LongAdder 对象,最后返回 Value。
  • 由于 computeIfAbsent 方法返回的 Value 是 LongAdder,是一个线程安全的累加器,因此可以直接调用其 increment 方法进行累加。

这样在确保线程安全的情况下达到极致性能,把之前 7 行代码替换为了 1 行。