蚂蚁花呗:阿里巴巴面试:Java 集合知识点(附图文解析)( 七 )


Segment 类Segment 类继承于 ReentrantLock 类 , 从而使得 Segment 对象能充当可重入锁的角色 。 一个 Segment 就是一个子哈希表 , Segment 里维护了一个 HashEntry 数组 , 并发环境下 , 对于不同 Segment 的数据进行操作是不用考虑锁竞争的 。
从源码可以看到 , Segment 内部类和我们上边看到的 HashMap 很相似 。 也有负载因子 , 阈值等各种属性 。
HashEntry 类HashEntry 是目前我们最小的逻辑处理单元 。 一个ConcurrentHashMap 维护一个 Segment 数组 , 一个Segment维护一个 HashEntry 数组 。
ConcurrentHashMap 类默认的情况下 , 每个ConcurrentHashMap类会创建16个并发的 segment , 每个 segment 里面包含多个 Hash表 , 每个 Hash 链都是由 HashEntry 节点组成的 。
put() 方法

  1. 定位segment并确保定位的Segment已初始化
  2. 调用 Segment的 put 方法 。

get() 方法get方法无需加锁 , 由于其中涉及到的共享变量都使用volatile修饰 , volatile可以保证内存可见性 , 所以不会读取到过期数据
JDK1.8 实现
ConcurrentHashMap 在 JDK8 中进行了巨大改动 , 光是代码量就从1000多行增加到6000行!1.8摒弃了Segment(锁段)的概念 , 采用了 CAS + synchronized 来保证并发的安全性 。
可以看到 , 和HashMap 1.8的数据结构很像 。 底层数据结构改变为采用数组+链表+红黑树的数据形式 。
和 HashMap1.8 相同的一些地方
  • 底层数据结构一致
  • HashMap初始化是在第一次put元素的时候进行的 , 而不是init
  • HashMap的底层数组长度总是为2的整次幂
  • 默认树化的阈值为 8 , 而链表化的阈值为 6
  • hash算法也很类似 , 但多了一步& HASH_BITS , 该步是为了消除最高位上的负符号 , hash的负在ConcurrentHashMap中有特殊意义表示在扩容或者是树节点

一些关键属性
put() 方法
  1. 首先会判断 key、value是否为空 , 如果为空就抛异常!
  2. spread()方法获取hash , 减小hash冲突
  3. 判断是否初始化table数组 , 没有的话调用initTable()方法进行初始化
  4. 判断是否能直接将新值插入到table数组中
  5. 判断当前是否在扩容 , MOVED为-1说明当前ConcurrentHashMap正在进行扩容操作 , 正在扩容的话就进行协助扩容
  6. 当table[i
    为链表的头结点 , 在链表中插入新值 , 通过synchronized (f)的方式进行加锁以实现线程安全性 。
    1. 在链表中如果找到了与待插入的键值对的key相同的节点 , 就直接覆盖
    2. 如果没有找到的话 , 就直接将待插入的键值对追加到链表的末尾
  7. 当table[i
    为红黑树的根节点 , 在红黑树中插入新值/覆盖旧值
  8. 根据当前节点个数进行调整 , 否需要转换成红黑树(个数大于等于8 , 就会调用treeifyBin方法将tabel[i
    第i个散列桶拉链转换成红黑树)
  9. 对当前容量大小进行检查 , 如果超过了临界值(实际大小*加载因子)就进行扩容

我们可以发现 JDK8 中的实现也是锁分离的思想 , 只是锁住的是一个 Node , 而不是 JDK7 中的 Segment , 而锁住Node 之前的操作是无锁的并且也是线程安全的 , 建立在原子操作上 。
get() 方法get 方法无需加锁 , 由于其中涉及到的共享变量都使用 volatile 修饰 , volatile 可以保证内存可见性 , 所以不会读取到过期数据


推荐阅读