面试阿里被质问:ConcurrentHashMap线程安全吗

没啥深入实践的理论系同学 , 在使用并发工具时 , 总是认为把HashMap改为ConcurrentHashMap , 就完美解决并发了呀 。 或者使用写时复制的CopyOnWriteArrayList , 性能更佳呀!技术言论虽然自由 , 但面对魔鬼面试官时 , 我们更在乎的是这些真的正确吗?
1 线程重用导致用户信息错乱生产环境中 , 有时获取到的用户信息是别人的 。 查看代码后 , 发现是使用了ThreadLocal缓存获取到的用户信息 。
ThreadLocal适用于变量在线程间隔离 , 而在方法或类间共享的场景 。若用户信息的获取比较昂贵(比如从DB查询) , 则在ThreadLocal中缓存比较合适 。问题来了 , 为什么有时会出现用户信息错乱?
1.1 案例使用ThreadLocal存放一个Integer值 , 代表需要在线程中保存的用户信息 , 初始null 。先从ThreadLocal获取一次值 , 然后把外部传入的参数设置到ThreadLocal中 , 模拟从当前上下文获取用户信息 , 随后再获取一次值 , 最后输出两次获得的值和线程名称 。
面试阿里被质问:ConcurrentHashMap线程安全吗文章插图
固定思维认为 , 在设置用户信息前第一次获取的值始终是null , 但要清楚程序运行在Tomcat , 执行程序的线程是Tomcat的工作线程 , 其基于线程池 。而线程池会重用固定线程 , 一旦线程重用 , 那么很可能首次从ThreadLocal获取的值是之前其他用户的请求遗留的值 。 这时 , ThreadLocal中的用户信息就是其他用户的信息 。
1.2 bug 重现在配置文件设置Tomcat参数-工作线程池最大线程数设为1 , 这样始终是同一线程在处理请求:
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 解决方案在finally代码块显式清除ThreadLocal中数据 。 即使新请求过来 , 使用了之前的线程 , 也不会获取到错误的用户信息 。修正后代码:
面试阿里被质问:ConcurrentHashMap线程安全吗文章插图
ThreadLocal利用独占资源的解决线程安全问题 , 若就是要资源在线程间共享怎么办?就需要用到线程安全的容器 。使用了线程安全的并发工具 , 并不代表解决了所有线程安全问题 。
1.4 ThreadLocalRandom 可将其实例设置到静态变量 , 在多线程下重用吗?current()的时候初始化一个初始化种子到线程 , 每次nextseed再使用之前的种子生成新的种子:
UNSAFE.putLong(t = Thread.currentThread(), SEED,r = UNSAFE.getLong(t, SEED) + GAMMA);如果你通过主线程调用一次current生成一个ThreadLocalRandom实例保存 , 那么其它线程来获取种子的时候必然取不到初始种子 , 必须是每一个线程自己用的时候初始化一个种子到线程 。可以在nextSeed设置一个断点看看:
UNSAFE.getLong(Thread.currentThread(),SEED);2 ConcurrentHashMap真的安全吗?我们都知道ConcurrentHashMap是个线程安全的哈希表容器 , 但它仅保证提供的原子性读写操作线程安全 。
2.1 案例有个含900个元素的Map , 现在再补充100个元素进去 , 这个补充操作由10个线程并发进行 。开发人员误以为使用ConcurrentHashMap就不会有线程安全问题 , 于是不加思索地写出了下面的代码:在每一个线程的代码逻辑中先通过size方法拿到当前元素数量 , 计算ConcurrentHashMap目前还需要补充多少元素 , 并在日志中输出了这个值 , 然后通过putAll方法把缺少的元素添加进去 。
为方便观察问题 , 我们输出了这个Map一开始和最后的元素个数 。
面试阿里被质问:ConcurrentHashMap线程安全吗文章插图
  • 访问接口
分析日志输出可得: