Java中的集合类——ConcurrentHashMap

JDK1.7之前的ConcurrentHashMap

介绍

(1)由于HashMap并非线程安全,且HashTable效率低下,故多线程下ConcurrentHashMap为首选。
(2)ConcurrentHashMap主要有三大结构:整个Hash表,segment(段),HashEntry(节点)。每个segment就相当于一个HashTable。
ConcurrentHashMap主要有三大结构:整个Hash表,segment(段),HashEntry(节点)。每个segment就相当于一个HashTable。
在这里插入图片描述
ConcurrentHashMap将锁加在segment上(每个段上),这样我们在对segment1操作的时候,同时也可以对segment2中的数据操作,这样效率就会高很多。
Segment是继承自ReentrantLock的。

ConcurrentHashMap的锁分段技术

(1)HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
(2)ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
(3)既然ConcurrentHashMap使用分段锁Segment来保护不同段的数据,那么在插入和获取元素的时候,必须先通过哈希算法定位到Segment。可以看到ConcurrentHashMap会首先使用Wang/Jenkins hash的变种算法对元素的hashCode进行一次再哈希。
(4)put时先lock再执行put操作,而get操作不需要加锁(get方法里用到的共享变量都定义成volatile,之所以不会读到过期的值,是根据 java 内存模型的 happen before 原则,对volatile 字段的写入操作先于读操作,即使两个线程同时修改和获取 volatile 变量,get操作也能拿到最新的值)。
(5)扩容时为了高效 ConcurrentHashMap 不会对整个容器进行扩容,而只对某个 segment 进行扩容。

JDK1.8下的ConcurrentHashMap

介绍

(1)JDK1.8下摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS 算法。它沿用了与它同时期的 HashMap 版本的思想,底层依然由“数组”+链表+红黑树的方式思想,大于 8 个转换为红黑树。默认初始大小 16,负载因子也是 0.75,定位元素的方法也是先 hashCode(),再无符号右移 16 位异或,再(n-1)&hash。
(2)取消 segments 字段,直接采用 transient volatile Node<K,V>[] table;保存数据,采用 table 数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

put函数流程

1、判断 put 进来的 key 和 value 是否为 null,如果为 null 抛异常。 (ConcurrentHashMap的 key、value 不能为 null)。
2、随后进入无限循环(没有判断条件的 for 循环),何时插入成功,何时退出。
3、在无限循环中,若 table 数组为空(底层数组加链表),则调用 initTable(),初始化 table;
4、若 table 不为空,先 hashCode(),再无符号右移 16 位异或,再(n-1)&hash,定位到table 中的位置,如果该位置为空(说明还没有发生哈希冲突),则使用 CAS 将新的节点放入 table 中。
5、如果该位置不为空,且该节点的 hash 值为 MOVED(即为 forward 节点,哈希值为-1,其中含有指向 nextTable 的指针,class ForwardingNode 中有 nexttable 变量),说明此时正在扩容,且该节点已经扩容完毕,如果还有剩余任务(任务没分配完)该线程执行helpTransfer 方法,帮助其他线程完成扩容,如果已经没有剩余任务,则该线程可以直接操作新数组 nextTable 进行 put。
6、如果该位置不为空,且该节点不是 forward 节点。对桶中的第一个结点(即 table表中的结点,哈希值相同的链表的第一个节点)进行加锁(锁是该结点,如果此时还有其他线程想来 put,会阻塞)(如果不加锁,可能在遍历链表的过程中,又有其他线程放进来一个相同的元素,但此时我已经遍历过,发现没有相同的,这样就会产生两个相同的),对该桶进行遍历,桶中的结点的 hash 值与 key 值与给定的 hash 值和 key 值相等,则根据标识选
择是否进行更新操作(用给定的 value 值替换该结点的 value 值),若遍历完桶仍没有找到hash 值与 key 值和指定的 hash 值与 key 值相等的结点,则直接新生一个结点并赋值为之前最后一个结点的下一个结点。
7、若 binCount 值达到红黑树转化的阈值,则将桶中的结构转化为红黑树存储,最后,增加 binCount 的值。最后调用 addcount 方法,将 concurrenthashmap 的 size 加 1,调用size()方法时会用到这个值。

扩容 transfer () 函数流程:

整个扩容操作分为两个部分:
第一部分是构建一个 nextTable,它的容量是原来的两倍,这个操作是单线程完成的。
第二个部分就是将原来 table 中的元素复制到 nextTable 中,这里允许多线程进行操作。
其他线程调用 helptransfer 方法来协助扩容时,首先拿到 nextTable 数组,再调用transfer 方法。给新来的线程分配任务(默认是 16 个桶一个任务)。
遍历自己所分到的桶:
1、桶中元素不存在,则通过 CAS 操作设置桶中第一个元素为 ForwardingNode,其 Hash值为 MOVED(-1),同时该元素含有新的数组引用。此时若其他线程进行 put 操作,发现第一个元素的 hash 值为-1 则代表正在进行扩容操作(并且表明该桶已经完成扩容操作了,可以直接在新的数组中重新进行 hash 和插入操作),该线程就可以去帮助扩容,或者没有任务则不用参与,此时可以去直接操作新的数组了。
2、桶中元素存在且 hash 值为-1,则说明该桶已经被处理了(本不会出现多个线程任务重叠的情况,这里主要是该线程在执行完所有的任务后会再次进行检查,再次核对)。
3、桶中为链表或者红黑树结构,则需要获取桶锁,防止其他线程对该桶进行 put 操作,然后处理方式同 HashMap 的处理方式一样,对桶中元素分为 2 类,分别代表当前桶中和要迁移到新桶中的元素。设置完毕后代表桶迁移工作已经完成,旧数组中该桶可以设置成ForwardingNode 了, 已经完成从 e table 复制到 e nextTable 的节点,要设置为 forward。

get函数流程

1、根据 k 计算出 hash 值,找到对应的数组 index
2、如果该 index 位置无元素则直接返回 null
3、如果该 index 位置有元素:
如果第一个元素的 hash 值小于 0,则该节点可能为 ForwardingNode 或者红黑树节点TreeBin。如果是 ForwardingNode(表示当前正在进行扩容,且已经扩容完成),使用新的数组来进行查找。
如果是红黑树节点 TreeBin,使用红黑树的查找方式来进行查找。如果第一个元素的 hash 大于等于 0,则为链表结构,依次遍历即可找到对应的元素,
也就是读的时候不会加锁,同时有 put,不会阻塞。
读不加锁是因为使用了 volatile(用在 transient volatile Node<K,V>[] table),happens-before。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值