面试阿里当场傻眼,被P8质问:ConcurrentHashMap真的线程安全吗?
没啥深入实践的初中级工程师,使用并发工具时,自以为把HashMap改为ConcurrentHashMap,就能完美解决并发。或者使用写时复制的CopyOnWriteArrayList,性能更佳呀!技术言论虽然自由,但面对P8魔鬼面试官时, 你能针对他提问的场景还能做出是否线程安全的正确判断吗?我们都知道ConcurrentHashMap是个线程安全的哈希表容器,但它仅保证提供的原子性读写操作线程安
没啥深入实践的初中级工程师,使用并发工具时,自以为把HashMap改为ConcurrentHashMap,就能完美解决并发。
或者使用写时复制的CopyOnWriteArrayList,性能更佳呀!
技术言论虽然自由,但面对P8魔鬼面试官时, 你能针对他提问的场景还能做出是否线程安全的正确判断吗?
我们都知道ConcurrentHashMap是个线程安全的哈希表容器,但它仅保证提供的原子性读写操作线程安全。
案例
有个含900个元素的Map,现在再补充100个元素进去,这个补充操作由10个线程并发进行。 开发人员误以为使用ConcurrentHashMap就不会有线程安全问题,于是不加思索地写出了下面的代码:在每一个线程的代码逻辑中先通过size方法拿到当前元素数量,计算ConcurrentHashMap目前还需要补充多少元素,并在日志中输出了这个值,然后通过putAll方法把缺少的元素添加进去。
为方便观察问题,我们输出了这个Map一开始和最后的元素个数。
访问接口分析日志输出可得: - 初始大小900符合预期,还需填充100个元素 - worker13线程查询到当前需要填充的元素为49,还不是100的倍数 - 最后HashMap的总项目数是1549,也不符合填充满1000的预期
bug 分析
ConcurrentHashMap就像是一个大篮子,现在这个篮子里有900个桔子,我们期望把这个篮子装满1000个桔子,也就是再装100个桔子。有10个工人来干这件事儿,大家先后到岗后会计算还需要补多少个桔子进去,最后把桔子装入篮子。 ConcurrentHashMap这篮子本身,可以确保多个工人在装东西进去时,不会相互影响干扰,但无法确保工人A看到还需要装100个桔子但是还未装时,工人B就看不到篮子中的桔子数量。你往这个篮子装100个桔子的操作不是原子性的,在别人看来可能会有一个瞬间篮子里有964个桔子,还需要补36个桔子。
ConcurrentHashMap对外提供能力的限制: - 使用不代表对其的多个操作之间的状态一致,是没有其他线程在操作它的。如果需要确保需要手动加锁 - 诸如size、isEmpty和containsValue等聚合方法,在并发下可能会反映ConcurrentHashMap的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。显然,利用size方法计算差异值,是一个流程控制 - 诸如putAll这样的聚合方法也不能确保原子性,在putAll的过程中去获取数据可能会获取到部分数据
解决方案
整段逻辑加锁:
只有一个线程查询到需补100个元素,其他9个线程查询到无需补,最后Map大小1000
既然使用ConcurrentHashMap还要全程加锁,还不如使用HashMap呢? 不完全是这样。
ConcurrentHashMap提供了一些原子性的简单复合逻辑方法,用好这些方法就可以发挥其威力。这就引申出代码中常见的另一个问题:在使用一些类库提供的高级工具类时,开发人员可能还是按照旧的方式去使用这些新类,因为没有使用其真实特性,所以无法发挥其威力。
案例
使用Map来统计Key出现次数的场景。 - 使用ConcurrentHashMap来统计,Key的范围是10 - 使用最多10个并发,循环操作1000万次,每次操作累加随机的Key - 如果Key不存在的话,首次设置值为1。
show me code:
有了上节经验,我们这直接锁住Map,再做 - 判断 - 读取现在的累计值 - +1 - 保存累加后值
这段代码在功能上的确毫无没有问题,但却无法充分发挥ConcurrentHashMap的性能,优化后:
ConcurrentHashMap的原子性方法computeIfAbsent做复合逻辑操作,判断K是否存在V,若不存在,则把Lambda运行后结果存入Map作为V,即新创建一个LongAdder对象,最后返回V 因为computeIfAbsent返回的V是LongAdder,是个线程安全的累加器,可直接调用其increment累加。
这样在确保线程安全的情况下达到极致性能,且代码行数骤减。
性能测试
- 使用StopWatch测试两段代码的性能,最后的断言判断Map中元素的个数及所有V的和是否符合预期来校验代码正确性
性能测试结果比使用锁性能提升至少5倍。
computeIfAbsent高性能之道
Java的Unsafe实现的CAS。 它在JVM层确保写入数据的原子性,比加锁效率高:
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
所以不要以为只要用了ConcurrentHashMap并发工具就是高性能的高并发程序。
辨明 computeIfAbsent、putIfAbsent
- 当Key存在的时候,如果Value获取比较昂贵的话,putIfAbsent就白白浪费时间在获取这个昂贵的Value上(这个点特别注意)
- Key不存在的时候,putIfAbsent返回null,小心空指针,而computeIfAbsent返回计算后的值
- 当Key不存在的时候,putIfAbsent允许put null进去,而computeIfAbsent不能,之后进行containsKey查询是有区别的(当然了,此条针对HashMap,ConcurrentHashMap不允许put null value进去)
文章末尾
欢迎各位大佬进群共同交流学习,我们的交流分享群:1149778920 暗号:CSDN
博主在这里给大家整理了包括但不限于:JAVA基础和进阶类、Spring、Spring boot、Spring MVC、MyBatis、MySQL、JVM等各种资料有,免费分享给各位进群的小伙伴
更多推荐
所有评论(0)