multiprocessing.manager管理的对象需要加锁吗_鏖战阿里面试被P8质问:ConcurrentHashMap真的线程安全吗?...

我们知道ConcurrentHashMap是个线程安全的哈希容器,但它仅保证提供原子性的读写操作是线程安全的。很多人不深刻理解此限制,造成很多使用错误,这在面试时就是致命的,会让面试官觉得你很菜!

假设

Map含初始元素,现再添一些元素,并由多线程并发执行。 开发人员自以为并发程序使用ConcurrentHashMap就不会有线程安全问题,于是:

386d3614ebe2dbd39552ce2a00d5069e.png

最后自行查看日志输出肯定是不符合期望。

使用不代表对其的多个操作之间的状态一致,是没有其他线程在操作它的。如果需要确保要手动加锁 - 诸如size、isEmpty和containsValue等聚合方法,在并发下可能会反映ConcurrentHashMap的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。显然,利用size方法计算差异值,是一个流程控制 - 诸如putAll这样的聚合方法也不能确保原子性,在putAll的过程中去获取数据可能会获取到部分数据

解决方案

整段逻辑加锁:

44deaa50ad99a0b3ed19f25805468ba0.png

观察日志可得只有一个线程查询到需补100个元素,其他线程查询到无需补,最后Map大小符合期望。

既然使用ConcurrentHashMap还要全程加锁,还不如使用HashMap呢?

ConcurrentHashMap提供了一些原子性的简单复合逻辑方法,用好这些方法就可以发挥其威力。所以要多看源码,熟悉真正的性能 API!

假设使用Map统计Key次数。

Key的范围是10 - 使用最多10个并发,循环操作1000万次,每次操作累加随机的Key - 如果Key不存在的话,首次设置值为1。

b16ebca3fb6f89e1f15c29c79efa53f3.png

有了上节经验,我们这直接锁住Map,再做 - 判断 - 读取现在的累计值 - +1 - 保存累加后值

这段代码在功能上的确毫无没有问题,但却无法充分发挥ConcurrentHashMap的性能,优化后:

d11744d62dac8bb1264af5e038d16649.png
  • ConcurrentHashMap的原子性方法computeIfAbsent做复合逻辑操作,判断K是否存在V,若不存在,则把Lambda运行后结果存入Map作为V,即新创建一个LongAdder对象,最后返回V 因为computeIfAbsent返回的V是LongAdder,是个线程安全的累加器,可直接调用其increment累加。

这样在确保线程安全的情况下达到极致性能,且代码行数骤减。

性能测试

  • 使用StopWatch测试两段代码的性能,最后的断言判断Map中元素的个数及所有V的和是否符合预期来校验代码正确性

07f249be98e60dd1522f8f147d41fee4.png

性能测试结果比使用锁性能提升至少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并发工具就是高并发程序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值