实现原理
先来看下JDK1.7
JDK1.7 中的 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。
如下图所示,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。
Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下
Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。Segment 默认为 16,也就是并发度为 16。并且不能懒惰加载。
存放元素的 HashEntry,也是一个静态内部类,主要的组成如下:
其中,用 volatile 修饰了 HashEntry 的数据 value 和 下一个节点 next,保证了多线程环境下数据获取时的可见性
再来看下JDK1.8
在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加细粒度的锁。
将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。
小结
Java 8 数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)
初始化,使用 cas 来保证并发安全,懒惰初始化 table
树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程
会用 synchronized 锁住链表头
put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素
添加至 bin 的尾部
get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 它会让 get 操作在新
table 进行搜索
扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可
做,它们会帮助把其它 bin 进行扩容,扩容时平均只有 1/6 的节点会把复制到新 table 中
size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加
即可
1、为什么ConcurrentHashMap不允许插入null值?
在Java语言中,给ConcurrentHashMap和Hashtable这些线程安全的集合中的Key或者Value插入 null(空) 值的会报空指针异常,但是单线程操作的HashMap又允许 Key 或者 Value 插入 null(空) 值。这到底是为什么呢?
那为什么要这么设计?
因为给ConcurrentHashMap中插入 null (空)值会存在歧义。我们可以假设ConcurrentHashMap允许插入 null(空) 值,那么,我们取值的时候会出现两种结果:
1、值没有在集合中,所以返回的结果就是 null (空);
2、值就是 null(空),所以返回的结果就是它原本的 null(空) 值。
这就产生歧义,那hashmap就不会产生歧义吗?
因为hashmap是单线程,如果取值为null,hashmap可以使用containsKey方法判断区分这个 null(空) 值到底是插入值是 null(空),还是本就没有才返回的 null(空) 值。
而concurrentHashMap是多线程,他取值为null,使用containsKey去判断的话,会被其他线程的添加null、删除null所干扰,无法判断null到底代表什么。
2、jdk8中,为什么使用synchronized替换 可重入锁 ReentrantLock?
jdk6中,对synchronized做了优化,添加了偏向锁、轻量级锁、重量级锁
3、ConcurrentHashMap 的并发度是什么?
jdk7中,segment的数量,默认是16,扩容后也是2的幂数。
jdk8中,是数组的大小
4、JDK1.7 与 JDK1.8 中ConcurrentHashMap 的区别
数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
保证线程安全机制:JDK1.7 采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8 采用CAS+synchronized保证线程安全。
锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。
链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash 冲突加剧,因此在链表节点数量大于 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。
查询时间复杂度:从 JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。
5、ConcurrentHashMap 和 Hashtable 的效率哪个更高?为什么?
ConcurrentHashMap 的效率要高于 Hashtable,因为 Hashtable 给整个哈希表加了一把大锁从而实现线程安全。而ConcurrentHashMap 的锁粒度更低,在 JDK1.7 中采用分段锁实现线程安全,在 JDK1.8 中采用CAS+synchronized实现线程安全。