魔鬼面试官必问:ConcurrentHashMap 线程安全吗?
码农突围
共 3587字,需浏览 8分钟
·
2022-01-19 14:49
这里是码农充电第一站,回复“666”,获取一份专属大礼包
真爱,请设置“星标”或点个“在看
HashMap
改为ConcurrentHashMap
,就完美解决并发了呀。或者使用写时复制的CopyOnWriteArrayList
,性能更佳呀!技术言论虽然自由,但面对魔鬼面试官时,我们更在乎的是这些真的正确吗?1 线程重用导致用户信息错乱
ThreadLocal
缓存获取到的用户信息。ThreadLocal
适用于变量在线程间隔离,而在方法或类间共享的场景。若用户信息的获取比较昂贵(比如从DB查询),则在ThreadLocal
中缓存比较合适。问题来了,为什么有时会出现用户信息错乱?1.1 案例
1.2 bug 重现
server.tomcat.max-threads=1
先让用户1请求接口,第一、第二次获取到用户ID分别是null和1,符合预期 用户2请求接口,bug复现!第一、第二次获取到用户ID分别是1和2,显然第一次获取到了用户1的信息,因为Tomcat线程池重用了线程。两次请求线程都是同一线程: http-nio-45678-exec-1
。
Tomcat服务器下跑的业务代码,本就运行在一个多线程环境(否则接口也不可能支持这么高的并发),并不能认为没有显式开启多线程就不会有线程安全问题 线程创建较昂贵,所以Web服务器会使用线程池处理请求,线程会被重用。使用类似ThreadLocal工具存放数据时,需注意在代码运行完后,显式清空设置的数据。
1.3 解决方案
1.4 ThreadLocalRandom 可将其实例设置到静态变量,在多线程下重用吗?
UNSAFE.putLong(t = Thread.currentThread(), SEED,
r = UNSAFE.getLong(t, SEED) + GAMMA);
UNSAFE.getLong(Thread.currentThread(),SEED);
2 ConcurrentHashMap真的安全吗?
2.1 案例
访问接口
初始大小900符合预期,还需填充100个元素 worker13线程查询到当前需要填充的元素为49,还不是100的倍数 最后HashMap的总项目数是1549,也不符合填充满1000的预期
2.2 bug 分析
使用不代表对其的多个操作之间的状态一致,是没有其他线程在操作它的。如果需要确保需要手动加锁 诸如size、isEmpty和containsValue等聚合方法,在并发下可能会反映ConcurrentHashMap的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制 。显然,利用size方法计算差异值,是一个流程控制 诸如putAll这样的聚合方法也不能确保原子性,在putAll的过程中去获取数据可能会获取到部分数据
2.3 解决方案
只有一个线程查询到需补100个元素,其他9个线程查询到无需补,最后Map大小1000
3 知己知彼,百战百胜
3.1 案例
使用ConcurrentHashMap来统计,Key的范围是10 使用最多10个并发,循环操作1000万次,每次操作累加随机的Key 如果Key不存在的话,首次设置值为1。
判断 读取现在的累计值 +1 保存累加后值
ConcurrentHashMap
的原子性方法computeIfAbsent
做复合逻辑操作,判断K是否存在V,若不存在,则把Lambda运行后结果存入Map作为V,即新创建一个LongAdder
对象,最后返回V 因为computeIfAbsent
返回的V是LongAdder
,是个线程安全的累加器,可直接调用其increment
累加。
3.2 性能测试
使用StopWatch测试两段代码的性能,最后的断言判断Map中元素的个数及所有V的和是否符合预期来校验代码正确性 性能测试结果:
3.3 computeIfAbsent高性能之道
static final boolean casTabAt(Node [] tab, int i,
Nodec, Node v) {
return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
辨明 computeIfAbsent、putIfAbsent
当Key存在的时候,如果Value获取比较昂贵的话,putIfAbsent就白白浪费时间在获取这个昂贵的Value上(这个点特别注意) Key不存在的时候,putIfAbsent返回null,小心空指针,而computeIfAbsent返回计算后的值 当Key不存在的时候,putIfAbsent允许put null进去,而computeIfAbsent不能,之后进行containsKey查询是有区别的(当然了,此条针对HashMap,ConcurrentHashMap不允许put null value进去)
3.4 CopyOnWriteArrayList 之殇
CopyOnWriteArrayList
缓存大量数据,而该业务场景下数据变化又很频繁。CopyOnWriteArrayList
虽然是一个线程安全版的ArrayList,但其每次修改数据时都会复制一份数据出来,所以只适用读多写少或无锁读场景。所以一旦使用CopyOnWriteArrayList
,一定是因为场景适宜而非炫技。CopyOnWriteArrayList V.S 普通加锁ArrayList读写性能
测试并发写性能 测试结果:高并发写,CopyOnWriteArray比同步ArrayList慢百倍 测试并发读性能 测试结果:高并发读(100万次get操作),CopyOnWriteArray比同步ArrayList快24倍
4 总结
4.1 Don't !!!
不要只会用并发工具,而不熟悉线程原理 不要觉得用了并发工具,就怎么都线程安全 不熟悉并发工具的优化本质,就难以发挥其真正性能 不要不结合当前业务场景,就随意选用并发工具,可能导致系统性能更差
4.2 Do !!!
认真阅读官方文档,理解并发工具适用场景及其各API的用法,并自行测试验证,最后再使用 并发bug本就不易复现, 多自行进行性能压力测试
-End-
最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!
点击👆卡片,关注后回复【面试题
】即可获取
评论