ConcurrentHashMap原理

143 阅读6分钟

基本知识

  • sizeCtl
  1. 数组为null时候,为0,代表数组还未初始化
  2. 多线程扩容时候,负数的低16位代表有多少线程正在扩容
  3. 正常情况下,代表数据需要扩容的阈值

简单总结下

  • put方法原理
  1. 对于key或者value为Null的,直接抛出空指针异常
  2. 首先会根据key的hashCode方法拿出其hash值,然后对hash的高16位和低16位做异或得到最后的hash值
  3. 开启死循环,首先判断数组是否初始化过,如果没有初始化数组
  4. 接着检查当前数组对应位置下,对象是否为Null,如果是直接通过CAS插入对象,如果插入失败,则继续当前循环检查。插入成功则结束循环。
  5. 如果当前数组对应位置下的对象Node的hash值是-1,那么表示这个时候其他线程中的map正在进行扩容,那么会调用方法helpTransfer去协助扩容
  6. 如果判断当前对应位置下是链表,那么直接对当前链表的头对象加synchroized,然后进行Node的插入。
  7. 如果判断对应位置是红黑树,那么依然对当前树的跟对象加锁,红黑树中插入该对象
  8. 上述循环结束后,会执行addCount方法来计算size大小,以及是否需要扩容。

ConCurrentHashMap原理.png

ConcurrentHashMap不可以传null的key和value

    假设可以传Null的可以和value,那么对于两个线程A和B,A通过compare判断key存在,B此时正好删除了该key,A接着调用get方法,此时会发现key不存在,返回了null,那么对于A来讲,A这时候不确定这个NUll是代表key的值还是代表key不存在

spread方法详解

    ConCurrentHashMap使用的是spread方法计算hash值,这个方法比Hashmap多了一步,当key的hashCode的高16位和低16位异或后,会再讲数据的高16位置为0,因为实际使用也只有低16位参与使用,防止高16位带来的影响。

helpTransfer 方法作用

    一句话概括,这个方法作用是用于多线程中,如果其他线程中的数组正在扩容,这个方法会协助其一起进行扩容。调用这个方法的时机是在put时候,发现当前位置的对象的hash值为-1(也就是MOVE常量)就会调用该方法辅助扩容,加快速度。

  • 原理

ConCurrentHashMap-helpsfer原理.png

putIfAbsent方法含义

    该方法和put方法区别是,该方法会判断当前map中对应key是否已经有value了,如果存在,则不会覆盖,继续使用现有旧值,如果没有,则将key和value存到Map中

关于get方法
  • 为什么get方法不用加锁呢?
  1. 因为对象中保存的tables是volitale修饰的,并且每个Node对象的下一个对象Node也是volatile修饰的,对于Node对象中的val也是volitale修饰的,这就保证了数据的可见性。
  2. 对于当前如果正在处于扩容阶段,但是当前的bucket已经迁移完成了,bucket中会存储一个ForwardingNode对象,该对象会保存新的newxtTable数组,接着会从这个数组中查找对应值。
  3. 如果当前扩容阶段,但是当前bucket还没有迁移完成,那么会从旧的根节点中查找Node对象。

注意这里从旧的根节点TreeBin中查找Node对象时候,会通过TreeBIn中保存的lockState数值,来暂停扩容,优先查找,查找结束再继续扩容。

对于get方法,如果当前正在扩容,那么会调用对应对象的find方法查找,会通过CAS对树节点lockState状态置为等待状态,等待get执行完后,再继续扩容。

ConcurrentHashMap的并发度

并发度就是同时支持多少个线程对COnCurretnhashmap进行操作。

  • jdk1.7中,由于每个segment中加一个锁,所以每个segment属于一个,那么segment有多长,并发度就是多少,默认是16。这个可以在实例化map时候设置,后期无法扩展的。这个值需要是2的倍数,且大于16。

  • jdk1.8中,由于对数据table中的每个Node对象单独加锁,所以其并发度是其数组table长度,默认也是16,扩容后是2的倍数。

和jdk1.7做对比

  • 原理
  1. jdk 1.7中主要保存了一个segment数组,然后对于每个segment对象下保存了一个HashEntry对象数组,HashEntry数组其实就是保存了Node对象或链表或红黑树。通过对segment继承于ReentrantLock来实现加锁操作。
  2. put时候,会先查找到对应的segment对象
  3. 取出对应的hashEntry数组,然后就是hashMap中put的那一套机制了。
jdk1.7怎么找到segment的下标呢?

    首先仍然是通过key的hashCode的高16和低16位得到新的hash 值,然后取该hash值的高4位和15做与得到在Segment数组中的下标。

迭代器

迭代器就是用于将Map中的key或者Node组装成迭代器,方便遍历取值。

  • 什么是强一致性和弱一致性

在遍历迭代器过程中,修改数据不会影响原数据结构的,称为弱一致性,如果修改迭代器中的数据会引起原数据结构中内容的变化,则为强一致性。

  • ConCurrenthashMap和hashmap

HashMap是强一致性,而ConCUrretnhahsMap是弱一致性。引深一下,ArrayList也是强一致性。 HashMap直接使用的原Map中的Node对象返回,而ConCurrenthashmp则是直接new一个对象放到迭代器中。

  • 为什么ConCUrretnhashMap和hashMap不一样呢

因为hashMap不考虑线程安全,所以迭代器也可以随意更改,但是ConCurretnhashMap需要考虑线程安全,所以如果是强一致性,那么就要到处加锁了。目前ConCUrrentHashMap中也会保存当前的map,当迭代器删除元素时候,调用map方法来删除。hashMap也是直接调用内部删除方法来删除。

  • 迭代器类型
  1. 线性迭代器,存储一个游标记录当前位置,ArrayList用的就是这种,因为其内部是数组,使用游标更方便
  2. 链式迭代器,类似指针指向下一位,hashMap用的这种,因为链表使用链式迭代更方便
  • 关于modCount

对于强一致性的数据结构Map和List中都存在modCount变量,每次发生put,delete等操作,该值都会加1 ,使用迭代器时候,会开始取下这个值,后续遍历过程中会时刻比对这个值,只要不一样发生了变化,说明其他地方有同时调用了put,delete等方法,就会抛出异常ConcurrentModificationException。对于ConCUrrenthashMap没这个问题,因为它是弱一致性,不会去记录这个操作,不用担心操作会有影响。

CAS机制

    CAS属于乐观锁,实际底层并没有使用synchroized等锁机制,CAS具有三个部分组成,内存地址,预期值和想要修改的值,每次去对应内存地址修改值时候,会先比对内存地址中的值和预期值是否一致,如果一致才会将想要修改的值放入到内存地址中,否则会返回失败给调用者,由调用者重新设置值。