专栏首页大宽宽的碎碎念从ConcurrentHashMap谈谈一致性

从ConcurrentHashMap谈谈一致性

你要是觉得我是要讲ConcurrentHashMap源码分析、segment,rehash之类的事情,就可以不用往下看了。

考虑以下场景:

在Spring Framework实现的服务中做一个计数器,要求对任意请求的到达计数。比如,实现了这处理这几个path的controller: /path1, /path2, /path3,……

很自然,基本的设计是做一个Service Bean,内部封装一个KV形式的方式来统计,像这样:

/path1 --> [count of path1]
/path2 --> [count of path2]
/path3 --> [count of path3]
...

在java里就是个map。而用得最多的就是HashMap了。那么基本的逻辑大概就是这样

@Service
public class CountService {
  Map counterMap = new HashMap();
  public void countPath(String path) {
    Integer count = counterMap.get(path);
    if (count == null) {
      counterMap.put(path, 1);
    } else {
      counterMap.put(path, count + 1);
    }
  }
}

懂得稍微多一点点的同学就会说,这种服务系统都是多线程的。确切的说,是Web Server内维持一个线程池,每个请求都会从线程池取一个线程出来。在同一个时刻,如果有多个Controller都在响应请求,这些Controller就在不同的线程中并发的执行。

这样的话,用HashMap就会有问题。HashMap不是线程安全的,在并发更改下它会报ConcurrentModificationException

那么有几个选择:

  • 自己包一个HashMap,然后让所有的get、put等方法都标记synchronized关键字强行加锁。
  • 使用Collections.synchronizedMap包装的HashMap
  • 使用ConcurrentHashMap

前两种本质上差不多。第三个听起来高大上,而且根据之前查阅的文章资料模模糊糊的印象表示ConcurrentHashMap可以有效减少加锁的几率,提高性能。另外,JDK8的ConcurrentHashMap完全用CAS避免了锁。太赞了,就用它呗。

成,试试就试试。

然后就会发现并不work。计数的结果,比实际发生的调用要少。

为什么咧????

这就是本文想说明的一个基本观点:一致性无法依靠单一组件解决;一致性要依靠正确的处理“需要同步的区域“才能解决

怎么理解?

要解决多线程一致性问题,第一步要做的是识别发生竞争的代码,并将其设计为同步的。在本例中,这段区域在下面这个过程中:

从counterMap中取出计数,+1,然后塞回counterMap

为了一致性,你必须把这一段同步化。你可以给这段逻辑起个名字叫做"getAndIncrAndPut",并且加锁。这时你会惊喜地发现,高大上的ConcurrentHashMap根本帮不上你的忙,因为它是一个通用的数据结构类,并没假设你会这么使用。ConcurrentHashMap只对基本的Map的方法put、get等提供同步支持,但不会把这段"getAndIncrAndPut"逻辑也给同步了。

这样的话只能自己封装了。当然,因为ConcurrentHashMap帮不上你的忙,你也用不着它了。

@Service
public class CountService {  
  Map counterMap = new HashMap(); 
  public synchronized void countPath(String path) {    
    Integer count = counterMap.get(path);   
    if (count == null) {      
      counterMap.put(path, 1);   
    } else {     
      counterMap.put(path, count + 1);    
    }  
  }
  public synchronized int getCountOfPath(String path) {
    Integer count = counterMap.get(path);   
    return count == null ? 0 : count;
  }
}

嗯,对,它加锁了,效率会有影响。如果觉得确实影响你的业务,可以借鉴ConcurrentHashMap的思路也对path做做分组,比如你可以弄2^N个counterMap,然后先对path做一次hash,选择对应的counterMap,就像ConcurrentHashMap的segment拆分逻辑那样。但是你必须在加锁的外围才能实现这一点。

简单来说,如果需要同步的区域恰好可以被封装到一个组件里(class,lib……),那么恭喜你,直接用就行了。但如果你的业务逻辑需要这个区域会跨多个组件,那么就只能对这个区域加锁。不管这个锁会让代码多难看,会让模块切分多不舒服,都必须得做。如果你的功力足够强,你可以自己封装一个符合你需求的组件来代替通用组件。

此外,还有其他几个变通的办法。

  • 串行化。把并行的请求变成一个序列依次执行。比如上面的例子,你可以用一个ConcurrentLinkedQueue收集所有的计数请求,然后在另一个计数线程消费这些请求。如果你用的是spring,可以考虑以下@Async,可以令你的代码简单不少。
  • 彻底单线程化。如果你用过nodejs就知道,根本就不会遇到这个问题,因为整个nodejs是一个单线程的系统。而且nodejs不一定比多线程的、静态编译的Java慢(why?另外撰文详细讨论)。如果多线程无可避免,可以把计数的工作交给redis,它也是个单线程的服务,而且提供了很赞的incr命令。
  • 简化问题。上面的分析会发现竞争的代码有两个地方。一个是对map的get和put操作。另外一个是对计数加1。对于前者,如果业务需求允许,可以考虑在初始化阶段将所有可能的path全部填入,之后就再也不需要put操作了(顺便用Collections.unmodifableMap包一下)。而对于计数,可以考虑用AtomicLong或者JDK8的LongAdder。他们都用CAS实现了高效的、原子的计数。这样一来,无需加锁,代码也简洁了许多。

也许你已经有点明白了,一致性问题之所以难,是因为无法通过抽象、组件化的方式掩盖这个问题。一旦出现,就必须得从头开始思考,并识别需要同步的区域。如果业务逻辑复杂到跨越多个Service,并且多个需要同步的区域相互重叠,那么代码就会相当难写。如果遇到了,希望你可以找到办法避开。

顺便提一下,还有一类问题也是这样难,无法通过单一组件解决,必须贯穿整个业务流。这个问题就是安全,也会在后面的文章中讲解。

那么ConcurrentHashMap到底解决了什么问题呢?—— 它成功的让Map这个接口的各个方法在并发情况下能使用,并且效率还不错;但除非是运气好,它不会解决你的业务问题

最后说一句,那个需要同步的区域术语一般叫做临界区(Critical Section)。希望这个词能帮你在面试里唬住人。

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 你对Redis的使用靠谱吗?Redis的性能高,吗?Redis可以保证原子性,吗?用Redis可以实现事务,吗?用Redis可以当队列,吗?Redis适合用来做什么?

    大宽宽
  • 如何看待编写业务代码

    大宽宽
  • 分布式系统如何定位压力问题监控监控什么呢实际的压力问题怎么发生的我用的工具

    大宽宽
  • 关于人工智能-微软和谷歌没有告诉你的是什么

    2018年9月,iFlytek,一家中国技术公司,人工智能的全球领导者(尤其是语音识别软件)在上海举行的技术会议上被指责将人工翻译伪装成机器翻译。举报人是Bel...

    银河1号
  • Coursera吴恩达《优化深度神经网络》课程笔记(1)-- 深度学习的实用层面

    Andrew Ng的深度学习专项课程的第一门课《Neural Networks and Deep Learning》的5份笔记我已经整理完毕。迷路的小伙伴请见如...

    红色石头
  • ThinkPHP page分页类的学习

    实例化数据对象 1.$user=M('user'); 查询总记录数 $count = $user->count();//直接读取数据表中条项总数 ...

    benny
  • 摩根大通将利用人工智能实现更好的交易操作

    据《商业内幕》网站2017年8月报道,摩根大通((JPMorgan))正在测试一个人工智能系统。 长期以来,主要金融服务公司一直在利用算法来提高股票交易效率。现...

    人工智能快报
  • 原 一场版本升级引发的性能血案 - 王者归

    老码农
  • 搜狗输入法的创新五字诀:『软件即服务』

    PC互联网时代,中国有哪些软件是完全原创而不是Copy2 China的?我第一个想到的是搜狗输入法。Windows自带了多种中文输入法,双拼、标准、五笔…选择很...

    罗超频道
  • 基于Apple IDFA的开源项目——SimulateIDFA

    什么是设备ID 设备ID是一个统称,只要符合设备ID这几个特点的设备参数都可以当做设备ID用,它的特点有三个: 唯一,只标记一部设备; 稳定,今天拿到设备ID是...

    CSDN技术头条

扫码关注云+社区

领取腾讯云代金券