没啥深入实践的理论系同学,在使用并发工具时,总是认为把HashMap
改为ConcurrentHashMap
,就完美解决并发了呀。或者使用写时复制的CopyOnWriteArrayList
,性能更佳呀!技术言论虽然自由,但面对魔鬼面试官时,我们更在乎的是这些真的正确吗?
我们都知道ConcurrentHashMap是个线程安全的哈希表容器,但它仅保证提供的原子性读写操作线程安全。
有个含900个元素的Map,现在再补充100个元素进去,这个补充操作由10个线程并发进行。
开发人员误以为使用ConcurrentHashMap就不会有线程安全问题,于是不加思索地写出了下面的代码:在每一个线程的代码逻辑中先通过size方法拿到当前元素数量,计算ConcurrentHashMap目前还需要补充多少元素,并在日志中输出了这个值,然后通过putAll方法把缺少的元素添加进去。
为方便观察问题,我们输出了这个Map一开始和最后的元素个数。
分析日志输出可得:
ConcurrentHashMap就像是一个大篮子,现在这个篮子里有900个桔子,我们期望把这个篮子装满1000个桔子,也就是再装100个桔子。有10个工人来干这件事儿,大家先后到岗后会计算还需要补多少个桔子进去,最后把桔子装入篮子。
ConcurrentHashMap这篮子本身,可以确保多个工人在装东西进去时,不会相互影响干扰,但无法确保工人A看到还需要装100个桔子但是还未装时,工人B就看不到篮子中的桔子数量。你往这个篮子装100个桔子的操作不是原子性的,在别人看来可能会有一个瞬间篮子里有964个桔子,还需要补36个桔子。
ConcurrentHashMap对外提供能力的限制:
整段逻辑加锁:
既然使用ConcurrentHashMap还要全程加锁,还不如使用HashMap呢?
不完全是这样。
ConcurrentHashMap提供了一些原子性的简单复合逻辑方法,用好这些方法就可以发挥其威力。这就引申出代码中常见的另一个问题:在使用一些类库提供的高级工具类时,开发人员可能还是按照旧的方式去使用这些新类,因为没有使用其真实特性,所以无法发挥其威力。
使用Map来统计Key出现次数的场景。
show me code:
有了上节经验,我们这直接锁住Map,再做
这段代码在功能上的确毫无没有问题,但却无法充分发挥ConcurrentHashMap的性能,优化后:
ConcurrentHashMap
的原子性方法computeIfAbsent
做复合逻辑操作,判断K是否存在V,若不存在,则把Lambda运行后结果存入Map作为V,即新创建一个LongAdder
对象,最后返回V
因为computeIfAbsent
返回的V是LongAdder
,是个线程安全的累加器,可直接调用其increment
累加。这样在确保线程安全的情况下达到极致性能,且代码行数骤减。
比使用锁性能提升至少5倍。
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并发工具就是高性能的高并发程序。
再比如一段简单的非 DB操作的业务逻辑,时间消耗却超出预期时间,在修改数据时操作本地缓存比回写DB慢许多。原来是有人使用了CopyOnWriteArrayList
缓存大量数据,而该业务场景下数据变化又很频繁。
CopyOnWriteArrayList
虽然是一个线程安全版的ArrayList,但其每次修改数据时都会复制一份数据出来,所以只适用读多写少或无锁读场景。
所以一旦使用CopyOnWriteArrayList
,一定是因为场景适宜而非炫技。
高并发写时,CopyOnWriteArrayList为何这么慢呢?因为其每次add时,都用Arrays.copyOf创建新数组,频繁add时内存申请释放性能消耗大。
参考
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。