Java.util.concurrent.ConcurrentHashMap原理JDK1.8

JDK1.8中实现线程安全的思想也已经完全变了,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想,大于8个转换为红黑树。数组默认初始大小16,负载因子也是0.75,定位元素的方法也是先hashCode(),再无符号右移16位异或,再(n-1)&hash。
取消segments字段,直接采用transient volatile Node[] 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[] table),是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值。

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可能已经不同了。

ConcurrentHashmap和Hashtable都不允许key和value为null,Collections.synchronizedMap和HashMap的key和value都可以为null(因为就是包装了hashmap),TreeMap的key不可为空(非线程安全,需要排序),value可以为null。

你可能感兴趣的:(Java.util.concurrent.ConcurrentHashMap原理JDK1.8)