专栏首页java达人ConcurrentHashMap使用示例

ConcurrentHashMap使用示例

作者:mononite

链接:https://my.oschina.net/mononite/blog/144329(点击文末阅读原文前往)

ConcurrentHashMap通常只被看做并发效率更高的Map,用来替换其他线程安全的Map容器,比如Hashtable和Collections.synchronizedMap。实际上,线程安全的容器,特别是Map,应用场景没有想象中的多,很多情况下一个业务会涉及容器的多个操作,即复合操作,并发执行时,线程安全的容器只能保证自身的数据不被破坏,但无法保证业务的行为是否正确。

举个例子:统计文本中单词出现的次数,把单词出现的次数记录到一个Map中,代码如下:

private final Map<String, Long> wordCounts = new ConcurrentHashMap<>();
public long increase(String word) {   
 Long oldValue = wordCounts.get(word);   
 Long newValue = (oldValue == null) ? 1L : oldValue + 1;
 wordCounts.put(word, newValue);    
 return newValue;
}

如果多个线程并发调用这个increase()方法,increase()的实现就是错误的,因为多个线程用相同的word调用时,很可能会覆盖相互的结果,造成记录的次数比实际出现的次数少。

除了用锁解决这个问题,另外一个选择是使用ConcurrentMap接口定义的方法:

public interface ConcurrentMap<K, V> extends Map<K, V> {   
 V putIfAbsent(K key, V value);    
 boolean remove(Object key, Object value);    
 boolean replace(K key, V oldValue, V newValue);    
 V replace(K key, V value);
}

这是个被很多人忽略的接口,也经常见有人错误地使用这个接口。ConcurrentMap接口定义了几个基于CAS(Compare and Set)操作,很简单,但非常有用,下面的代码用ConcurrentMap解决上面问题:

private final ConcurrentMap<String, Long> wordCounts = new ConcurrentHashMap<>();
public long increase(String word) {   
 Long oldValue, newValue;   
  while (true) {
      oldValue = wordCounts.get(word);    
      if (oldValue == null) { 
      // Add the word firstly, initial the value as 1
          newValue = 1L;            
          if (wordCounts.putIfAbsent(word, newValue) == null) { 
               break;
           }
        } else {
           newValue = oldValue + 1;            
           if (wordCounts.replace(word, oldValue, newValue)) {  
               break;
           }
        }
    }    
   return newValue;
}

代码有点复杂,主要因为ConcurrentMap中不能保存value为null的值,所以得同时处理word不存在和已存在两种情况。

上面的实现每次调用都会涉及Long对象的拆箱和装箱操作,很明显,更好的实现方式是采用AtomicLong,下面是采用AtomicLong后的代码:

private final ConcurrentMap<String, AtomicLong> wordCounts = new ConcurrentHashMap<>();
public long increase(String word) {
   AtomicLong number = wordCounts.get(word);   
   if (number == null) {
     AtomicLong newNumber = new AtomicLong(0);       
     number = wordCounts.putIfAbsent(word, newNumber);      
     if (number == null) {       
          number = newNumber;
      }
   }    
    return number.incrementAndGet();
}

这个实现仍然有一处需要说明的地方,如果多个线程同时增加一个目前还不存在的词,那么很可能会产生多个newNumber对象,但最终只有一个newNumber有用,其他的都会被扔掉。对于这个应用,这不算问题,创建AtomicLong的成本不高,而且只在添加不存在词是出现。但换个场景,比如缓存,那么这很可能就是问题了,因为缓存中的对象获取成本一般都比较高,而且通常缓存都会经常失效,那么避免重复创建对象就有价值了。下面的代码演示了怎么处理这种情况:

private final ConcurrentMap<String, Future<ExpensiveObj>> cache = new ConcurrentHashMap<>();
public ExpensiveObj get(final String key) {
    Future<ExpensiveObj> future = cache.get(key); 
    if (future == null) {
        Callable<ExpensiveObj> callable = new Callable<ExpensiveObj>() {
    
          @Override          
          public ExpensiveObj call() throws Exception { 
    
              return new ExpensiveObj(key);
          }
        };
        FutureTask<ExpensiveObj> task = new FutureTask<>(callable);

        future = cache.putIfAbsent(key, task);        
    
         if (future == null) {
            future = task;       
            task.run();
        }
    } 
    
     try {     
        return future.get();
    } catch (Exception e) {
        cache.remove(key);    
    
         throw new RuntimeException(e);
    }
}

解决方法其实就是用一个Proxy对象来包装真正的对象,跟常见的lazy load原理类似;使用FutureTask主要是为了保证同步,避免一个Proxy创建多个对象。注意,上面代码里的异常处理是不准确的。

最后再补充一下,如果真要实现前面说的统计单词次数功能,最合适的方法是Guava包中AtomicLongMap;一般使用ConcurrentHashMap,也尽量使用Guava中的MapMaker或cache实现。

本文分享自微信公众号 - java达人(drjava),作者:mononite

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-03-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • OAuth2 登陆授权代码示例

    现如今各大互联网公司都提供了自己的开放平台,这给第三方开发者提供了不少机会,这些平台为了让开发者访问平台内部被保护的特定资源,使用了OAuth2作为登陆授权协议...

    java达人
  • 在https中传递查询字符串的安全性

    译者:java达人-卍极客 英文链接: http://blog.httpwatch.com/2009/02/20/how-secure-are-query-st...

    java达人
  • 深入解析Java中Flushable接口的flush方法

    今天写这篇文章是为了纪念同事讲得两句话:1、flush =在后面对out使劲的抽一鞭子,并命令“赶紧给我写入,我的水桶太满了”;2、写入数据量不大时,可以考虑不...

    java达人
  • 工业机器人的运行结构

    手臂是机器人执行机构中重要的部件,它的作用是将抓取的工件运送到给定的位置上, 因而一般机器人的手臂有3个自由度,即手臂的伸缩、左右回转和升降(或俯仰)运动。手 ...

    机器人网
  • Angular 界面元素CSS样式的条件式施加方式

    我有一个Angular列表,我期望在元素li被点击时,显示的外观和其他元素不一样。可以通过li被点击时,给该元素分配一个CSS样式的方式来实现。

    Jerry Wang
  • 加州无人车路测新添Lyft,中国公司Roadstar和长安也要去跑一跑

    李根 发自 LHY 量子位 报道 | 公众号 QbitAI 美国加州交通管理局(DMV)最新更新的文件显示,又有6家公司获得路测牌照,获准在加州进行无人车路测...

    量子位
  • ContentProvider简介

    (一) 基础知识 Content Provider属于Android四大组件之一,相比较而言,它更侧重于共享数据。Android的数据存储方式有以下几种:...

    QQ音乐技术团队
  • 字节跳动估值超过3400亿?今日头条和抖音成国民级应用

    大家可能都知道今日头条或抖音,但是不一定听说过字节跳动。可以告诉大家的是,字节跳动是今日头条、抖音、西瓜视频等众多国民级应用背后的母公司。这家公司的掌舵人就是张...

    光荣与梦想1987
  • 【leetcode刷题】T127-二叉树的后序遍历

    https://leetcode-cn.com/problems/binary-tree-postorder-traversal/

    木又AI帮
  • SEO那些事:一句代码一键分享网站

    王小婷

扫码关注云+社区

领取腾讯云代金券