ConcurrentHashMap面试题总结

1、简要介绍

Java7 中 ConcurrentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。

Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。

2、jdk1.5之前,jdk提供的Map同步容器有:

1、使用Hashtable类。Hashtable使用synchronized同步锁修饰put、get、remove等方法,读写只能串行进行,性能低下。

2、使用Collections.synchronizedMap返回一个同步代理类synchronizedMap。可以将指定集合包装成线程同步的集合。

3、

ConcurrentHashMap继承了Abstract抽象类,实现了ConcurrentMap接口。

Abstract抽象类减少了实现Map数据结构的工作量,ConcurrentMap接口提供了线程安全性和原子保证。

4、节点类型有5种:只有Node节点和TreeNode节点存储着真实的数据。
1、Node:连接着一个链表。

2、TreeBin:红黑树的顶级节点,hash值固定为-1,TreeBin连接着一个红黑树,红黑树节点时TreeNode,TreeBin指向红黑树的根节点。

3、TreeNode:红黑树中存储数据的节点。

4、ForwrdingNode:辅助节点,临时节点,hash值固定为-1,在扩容进行的时候才会出现,相当于一个占位节点。扩容的时候,当table数组一个hash桶的节点全部迁移到新数组之后,原table数组的桶中会被放置一个ForwrdingNode节点。

5、ReservationNode:保留节点,占位符。hash值固定为-3。在加锁的时候会用到ReservationNode起到占位符的作用。

问:为什么使用红黑树桶的顶级节点不直接使用TreeNode,而是使用TreeBin?

1、红黑树的操作比较复杂,有左旋、右旋等等,ConcurrentHashMap使用TreeBin作为红黑树的代理节点封装了这些操作,这是一种职责分离的思想,降低了TreeNode的复杂度。

2、红黑树删除和添加结点的时候,会有一个平衡的过程。根节点变成孩子节点,孩子节点可能变成根节点。如果以TreeNode作为红黑树的顶级节点,由于插入和删除时根节点的不确定性,就不可能通过在顶级节点加同步锁来实现线程安全。

5、在构造函数中没有创建实际的table数组,只有在首次插入成功后,才会初始化容量初始化数组。容量是2的次幂值。最高位是符号位,数值位只有31位可用,所以最高容量是2的30次幂。

sizeCtl是一个重要属性。

  • sizeCtl=0,默认值,表示table初始化的时候使用默认容量DEFAULT_CAPACITY。
  • sizeCtl>0,有两种含义:
    • 如果table未初始化,sizeCtl表示table初始化时的容量。
    • 如果table已经初始化,sizeCtl表示table扩容的阈值。
  • sizeCtl=-1,表示有线程正在初始化table数组操作。
  • sizeCtl=-(1+nThreads),表示有nThreads个线程正在进行扩容操作。

指定容量的构造函数中调用了一个tableSizeFor()方法,计算初始容量。初始容量为:initialCapacity + (initialCapacity >>> 1) + 1的最小2的次幂值。

table数组长度保持2的次幂值,有两个好处:1、插入数据时计算索引位置更高效。2、计算扩容后的索引位置更高效。

6、put方法:

调用了putVal方法。主要逻辑是将数据插入到对应索引的桶中,数据插入成功后判断是否需要将链表转化成红黑树,最后判断table数组是否需要扩容。

可以分成两个阶段:

1、将数据插入到table中;

2、更新ConcurrentHashMap中的元素总数。

第一阶段:

一共处理了四种情况:

1、table未初始化。先初始化table。初始化调用的initTable()方法。

2、table对应索引位置桶是为空的。直接将数据放入table[i]即可。使用CAS操作将数据放入桶中。如果CAS失败,说明其他线程抢先CAS操作成功。当前线程重试。

3、table[i]的桶不是空的。如果table[i]的位置是ForwardingNode节点,说明table正在扩容。当前线程需要先尝试协助进行数据迁移。

4、table[i]的桶中已经有了数据节点,出现hash冲突。分为两种:链表或者红黑树。

  1. 首先对table[i]]加同步锁,同时检验table[i]是否被修改。防止其他写线程修改。
  2. 加锁成功后,判断是链表还是红黑树。通过节点的hash值来判断,如果大于0,桶的类型是链表。小于0,是红黑树。然后按照链表或者红黑树插入节点的方式插入就好了。

插入成功后,如果链表上的节点个数大于等于8了,会使用treefyBin()方法将链表转成红黑树。转之前会先检查数组成都是否小于64,小于的话先进行扩容操作。

在treefyBin方法中,实际上维护了两种数据结构,一种是红黑树,另一种是双向链表。

treefyBin方法先遍历单向Node链表,创建双向链表,然后创建TreeBin实例,将双向链表包装成红黑树。最后将TreeBin实例返回到table[i]中。

initTable()方法使用了CAS无锁策略。保证同时只能有一个线程执行初始化table的工作。主要逻辑:

1、检查是否进行了初始化操作,如果还没初始化,进行初始化。如果已经初始化了,就直接退出initTable()方法。

2、开始初始化。处理了两种情况:1、table还未初始化。2、table已经开始了初始化。初始化之前设置sizeCtl=-1,表示有线程正在初始化。其他线程进入了,这时sizeCtl=-1,会调用Thread.yield()方法,让出CPU执行权。

初始化容量和扩容阈值的计算方法:

1、table初始容量:如果sizeCtl=0,初始化使用默认容量16。如果sizeCtl>0,初始化容量为sizeCtl。

2、扩容阈值:0.75*n,用作下一次扩容的阈值。

在put方法中,检验table[i]是否被修改用到了tabAt(table, index)方法,获取table中index位置的最新值。tabAt()方法通过Unsafe的getObjectValue()方法获取。getObjectValue()以volatile读的方式获取对象obi中内存偏移量offset的最新值。

请问:为什么不直接使用table[i]来获取元素呢?

答:虽然table数组本身就是volatile变量,但是volatile类型的数组只针对数组的引用具有volatile的可见性语义,而非里面的元素,所以table[i]有可能不是最新值。

第二阶段:

如果是新增节点插入,需要执行addCount()更新计数器,将计数器加1。

7、get方法获取元素:分成三种情况。

  1. table[i]上的节点key和待查找的key相等,直接返回value即可。
  2. 如果table[i]上的节点hash值小于0,说明table[i]不是Node节点。而是TreeBin/ForwardingNode/ReservationNode三种节点之一。通过对应节点的find方法查找匹配的节点,并返回节点的value。
  3. table[i]上的节点是链表节点,遍历链表查找。

8、size()方法

ConcurrentHashMap键值对计数时,使用了分段锁的思路。计数相关的字段:baseCount和ocuntCells数组。

当没有并发冲突时,直接使用baseCount计数。所有的计数都会记录在该变量上。

当有并发冲突时,计算每个线程对应的索引位置,计数会被更新到counterCells数组对应位置。

9、扩容与数据迁移

包含两个步骤:首先是数组扩容。新建一个2倍于原来容量的新数组。这一步需要保证只能由一个线程完成。然后进行数据迁移,把旧数据重新计算在新数组中桶的位置再转移到新数组中。

数据迁移这一步可以多线程完成。如果两个不同的键值key1和key2如果在table中索引位置不相同,那么迁移到新数组的索引位置必然也不相同,所以每个桶节点的迁移不会相互影响。于是,我们可以用分段的方式,将整个table数组的桶按段划分,每一段包含一定索引区间的桶,将不同的段分给不同的线程,分别进行迁移。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值