【面向面试学习】ConcurrentHashMap如何实现并发访问?

265 阅读2分钟

前言

在看本文之前,最好先百度一下一下几个方面,以下几个方面不在本文讨论范围内,但是会引用到

  1. Java8之前的HashMap
  2. Java8之后的HashMap
  3. Java8之前的ConcurrentHashMap
  4. Java8之后的ConcurrentHashMap
  5. 无锁CAS操作,Java UnSafe
  6. volatile、synchronized关键字

圈定并发点

之所以Java引入ConcurrentHashMap就是为了解决,多线程场景下,同时操作一个HashMap会产生数据异常的问题,常用的主要有以下几个操作

  • put 向map添加数据
  • resize map扩容
  • addCount 计数
  • replace 替换map里一个数据
  • remove 移除map里的对象
  • clear 清除map里的一个或全部对象

其实精炼一下,就是三点

  • 增删改操作 这些都是对map的写操作,必然要考虑多线程问题
  • 扩容操作 对存储数据结构的,整体拷贝复制,必然要考虑
  • 计数操作 增删必然会影响计数

具体分析

增删改操作

ConcurrentHashMap在增删改上施加的线程安全机制是近似的,以下以Put方法为例

public void put(K key, V value){

  1. 判断K,V是否非空,ConcurrentHashMap要求KV不能为空
  2. 根据K计算Hash值
  • Node数组死循环开始
  1. Node数组如果为空初始化这个数组,初始化中,若有线程进入这里Thread.yield(),让其等待,同时使用UnSafe.compareAndSwapInt保证在初始化数组时的多线程安全,这里的Node和Hashmap的Node主要区别在val和next是volatile的即保证线程可见性
  2. 初始万数组后或数组不为null,使用UnSafe.getObjectVolatile查询hash指向的数组位置是否为空,若为空通过UnSafe.compareAndSwapObject把这个KV组成的Node插入Node数组
  3. 如果上一步UnSafe的方式查询发现那个位置不为空有node,且这个hash值为负,则来到这里进入扩容处理
  4. 如果上一步的hash值为正,synchronized (f),f是hash值,这里对这个node对应的链表做查找,由于一上来就synchronized ,后面的操作就线程安全了,根据K在这个Node的链表上是否有一样的K如果有覆盖V,如果没有,在链表上增加一个Node存储,Jdk1.8以后可能还涉及到这个链表长度超过8,要转为红黑树,以及树的总结点数少于6时,重新转换为链表的操作
  • Node数组死循环结束
  1. map计数器+1

}

说完Put方法的逻辑,能看出多个针对多线程操作的特殊操作,主要是利用,UnSafe提供底层方法,用CAS的方式,对数组进行查询、增加修改,CAS是与锁迥然不同的多线程处理方式,效率更高,同时还使用了常见synchronized,这个已经经过多次优化,性能不错的关键字。可自行查看remove,clear的方法,大致逻辑也是类似。

参考 java1.8 ConcurrentHashMap 详细理解