-
并发工具类的“坑”,你遇到过哪些?
前景:
在我们小伙伴与同事朋友讨论时,我们有时会听到有关线程安全和并发工具的一些片面的观点和结论。
比如“把 HashMap 改为 ConcurrentHashMap,就可以解决并发问题了呀”“要不我们试试无锁的 CopyOnWriteArrayList 吧,性能更好”和“存在并发问题?加锁就可以了呀”等等等....
其实,事实并没有那么简单~
使用了并发工具,并不能完全解决并发问题
大家都知道,JDK1.5之后推出ConcurrentHashMap,是一个高性能的线程安全的哈希表容器。
可能大家会认为,这是一个很好地解决并发的工具,其实并不然。
“线程安全”这四个字特别容易让人误解,因为 ConcurrentHashMap 只能保证提供的原子性读写操作是线程安全的。
在一些我们实际的业务开发场景中,如果我们一昧的去使用,而不去剖析,这样“Bug” 就会找到你~
栗子:
有个Map已经存储900个元素,现在要在增加100个元素,size为1000。增加100个元素,我们需要10个线程并发运行。
可能有小伙伴会不假思索的来用ConcurrentHashMap,认为不会出现什么并发问题。
小明(假设人物)看到需求,觉得不是什么问题,于是使用ConcurrentHashMap来操作。
代码逻辑:在每一个线程的代码逻辑中先通过 size 方法拿到当前元素数量,计算 ConcurrentHashMap 目前还需要补充多少元素,并在日志中输出了这个值,然后通过 putAll 方法把缺少的元素添加进去。起始元素个数和最终元素个数日志打印。
小明信心满满去自测,看了日志他傻眼了,如下:
从日志中可以看到:
- 所需要填入元素为100,符合预期
- 其中一个线程查询所需要元素数为36
- 还有一个线程查询到需要填充的元素数是负的,显然已经过度填充了
- 最终元素个数也是1536,不符合预期
那么为什么会出现这些种种问题呢? 让我们继续Look。
针对这个问题,我们假设一个场景:搬砖(不是码农搬砖哦)。工地上,有个卡车要从工地拉走一车砖,车的容量可装1000块砖,10位打工人同时搬砖。打工人辛苦一上午已经完工90%,还有仅剩的100块。ConcurrentHashMap就是这个车本身,可以确保多个打工人在搬砖时,不会相互影响干扰,但无法确保打工人 A 看到还需要装 100 块砖但是还未装的时候,工人 B 就看不到车中的砖数量。更重要的是搬砖这个操作不是原子性的,别人看起来可能车里一下变成了944、964等,还需要补不一样的数量。
回到咱们的ConcurrentHashMap,映射出:
- 使用ConcurrentHashMap时,不能确保多个操作的状态是一致的,如若确保,需增加锁。
- ConcurrentHashMap中size()、isEmpty()等等,参数只能当做参考,不能当做控制逻辑。
- 当然ConcurrentHashMap中putAll()并不能确保原子性,很有可能putAll获取到的只是部分数据
如何解决目前出现问题呢?
有两种解决方法,在这里都列举一下:
1. 直接针对ConcurrentHashMap加上锁synchronized。这种全程ConcurrentHashMap加锁,可能发挥不到ConcurrentHashMap的效率最大化。
2. 使用ConcurrentHashMap 的原子性方法 computeIfAbsent 来做复合逻辑操作,判断 Key 是否存在 Value,如果不存在则新创建一个 LongAdder 对象,最后返回 Value。由于 computeIfAbsent 方法返回的 Value 是 LongAdder,是一个线程安全的累加器,因此可以直接调用其 increment 方法进行累加。
经过小明同学的不懈努力,终于将代码效率提升了一个档次,安全问题解决,同时得到了领导对他的认可。
尾声:computeIfAbsent 为什么如此高效呢?
期待各位大佬评论区讨论~~ 当然有任何问题欢迎私信和留言~~~~~~~
谢谢各位支持!
欲知CopyOnWriteArrayList有什么坑,请看下章分析。
注:转载请注明出处:https://www.cnblogs.com/jiahui-chen/