Concurrent Program——深入理解ConcurrentHashMap

 HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率

ConcurrentHashMap主要有三大结构:整个Hash表,segment(段),HashEntry(节点)。每个segment就相当于一个HashTable。ConcurrentHashMap将锁加在segment上(每个段上),这样我们在对segment1操作的时候,同时也可以对segment2中的数据操作,这样效率就会高很多。

Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。Put和remove方法中有lock()和unlock()(都是使用的this对象,lock()在代码开始,unlock在finally中)。

Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素.

segments数组的长度ssize通过concurrencyLevel(并发等级默认是16)计算得出。为了能通过按位与的哈希算法来定位segments数组的索引,必须保证segments数组的长度是2的N次方(power-of-two size),所以必须计算出一个是大于或等于concurrencyLevel的最小的2的N次方值来作为segments数组的长度。

初始化每个Segment。输入参数initialCapacity是ConcurrentHashMap的初始化容量,loadfactor是每个segment的负载因子,在构造方法里需要通过这两个参数来初始化数组中的每个segment。segment里HashEntry数组的长度,它等于initialCapacity除以ssize的倍数(总大小除以segments数组长度),HashEntry的长度也是2的N次方

我们知道HashTable容器的get方法是需要加锁的,那么ConcurrentHashMap的get操作是如何做到不加锁的呢?原因是它的get方法里将要使用的共享变量都定义成volatile,之所以不会读到过期的值,是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值。

如何扩容。扩容的时候首先会创建一个两倍于原容量的数组,然后将原数组里的元素进行再hash后插入到新的数组里。为了高效ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容

其中有一个Segment数组,每个Segment中都有一个锁,因此Segment相当于一个多线程安全的HashMap,采用分段加锁。每个Segment中有一个Entry数组,Entry中成员value是volatile修饰,其他成员通过final修饰。get操作不用加锁,put和remove操作需要加锁,因为value通过volatile保证可见性。

两个hash过程,第一次找到所在的桶,并将桶锁定,第二次执行写操作。而读操作不加锁

Collections.SynchronizedMap和Hashtable都是整个表的锁,与ConcurrentHashMap锁粒度不同*

ConcurrentHashMap不允许key或value为null值。

ConcurrentHashMap允许一边更新、一边遍历,也就是说在Iterator对象遍历的时候,ConcurrentHashMap也可以进行remove,put操作,且遍历的数据会随着remove,put操作产出变化,相当于有多个线程在操作同一个map(可以在foreach keysetremove对象,HashMap不可以)

java.util.concurrent.ConcurrentHashMap<K,V> JDK1.8

本文的分析的源码是JDK8的版本,与JDK6的版本有很大的差异。实现线程安全的思想也已经完全变了,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想,大于8个转换为红黑树。默认初始大小16,负载因子也是0.75,定位元素的方法也是先hashCode(),再无符号右移16位异或,再(n-1)&hash

取消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了,已经完成从table复制到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

ConcurrentHashmap和Hashtable不允许key和value为null:

ConcurrentHashmap和Hashtable都是支持并发的,这样会有一个问题,当你通过get(k)获取对应的value时,如果获取到的是null时,你无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射。HashMap是非并发的,可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains(key)和m.get(key),m可能已经不同了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值