在了解 HashMap 的的原理时,对于 jdk7 和 8 的实现是不同的,同样,对于支持并发的 ConcurrentHashMap 来说其实现也不相同。
其主要区别在于 两者保证线程安全的机制不同 ,jdk7 采用的是 分段锁 的概念,每一个分段都有一把锁,锁内存储的着数据,锁的个数在初始化之后不能扩容。
而 jdk8 的 ConcurrentHashMap 数据结构同 HashMap,通过 Synchronized+CAS 来保证其线程安全。
jdk7
在 jdk7 中,有一个非常重要的概念就是 Segment
,实际上我们发现,同 HashMap 的设计一样,它也是用来存储数据的一个变量。
/** * The segments, each of which is a specialized hash table. * 表示 每一段都是一个hash表 */ final Segment[] segments; 复制代码
在 Segment
这个内部类中,有一个 table
变量,在 HashMap 中存储数据也是叫 table
的变量。
这里叫 HashEntry,而 HashMap 中就叫 Entry,其内部成员变量都大致相同,HashEntry 如下:
static final class HashEntry{ final int hash; final K key; volatile V value; volatile HashEntry next; HashEntry(int hash, K key, V value, HashEntry next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } ...... } 复制代码
再去看 Segment
这个类:
static final class Segmentextends ReentrantLock implements Serializable { ...... } 复制代码
可以看到它继承了 ReentrantLock,因此可以实现加锁操作,而 Segment
有段、片的意思,因此通常叫做分段锁。
所以我们可以得出 ConcurrentHashMap 的结构大致为:
ConcurrentHashMap 是由一个个 Segment 组成的,并且每一个 Segment 包含了一个 HashEntry 数组,数组中的每一个 HashEntry 就是存储的数据。
用一张图来描述它:
结构知道了,我们现在看看 put()
方法:
public V put(K key, V value) { Segments; if (value == null) throw new NullPointerException(); // 1.为输入的key做 hash 运算,得到 hash 值; int hash = hash(key); // 2.通过hash值,定位到对应的 Segment 对象; int j = (hash >>> segmentShift) & segmentMask; // 3.检查segment[j]是否已经初始化了,没有的话调用ensureSegment初始化segment[j] if ((s = (Segment )UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment s = ensureSegment(j); // 4.向片段中插入键值对,加锁操作 return s.put(key, hash, value, false); } 复制代码
这里不想深入了,太复杂,想了解的话可参考: ConcurrentHashMap 1.7 源码解读
jdk8
在看看 jdk8 版本,他主要做了 2 处改动:
transient volatile Node[] table
保存数据,采用 table 数组元素作为锁,从而实现了对每一行数据进行加锁,并发控制使用 Synchronized 和 CAS 来操作;不同于 jdk7 的HashEntry,jdk8 中叫 Node,结构类似:
static class Nodeimplements Map.Entry { final int hash; final K key; volatile V val; volatile Node next; ...... } 复制代码
还有用于存储红黑树的数据的存储结构 TreeNode:
static final class TreeNodeextends Node { TreeNode parent; // red-black tree links TreeNode left; TreeNode right; TreeNode prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash, K key, V val, Node next, TreeNode parent) { super(hash, key, val, next); this.parent = parent; } ...... } 复制代码
用一张图来描述它的结构:
这个结构和 HashMap 的结构实现基本一致,只是为了保证线程安全而使得其实现变复杂。
put()
方法
① 根据 key 计算出 hashcode ;
② 判断是否需要进行初始化;
③ f
即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功;
④ 如果当前位置的 hashcode == MOVED == -1
,则需要进行扩容,相对于 HashMap 要复杂很多;
⑤ 如果都不满足,则利用 synchronized 锁写入数据;
⑥ 如果数量大于 TREEIFY_THRESHOLD
则要转换为红黑树。
在 ConcurrentHashMap 中通过一个 Node
数组来保存添加到 map 中的键值对,而在同一个数组位置是通过链表和红黑树的形式来保存的。但是这个数组只有在 第一次 添加元素的时候才会初始化,否则只是初始化一个ConcurrentHashMap 对象的话,只是设定了一个 sizeCtl
变量,这个变量用来判断对象的一些状态和是否需要扩容。
第一次添加元素的时候,默认初期长度为 16,当往 map 中继续添加元素的时候,通过 hash 值跟数组长度取与来决定放在数组的哪个位置,如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了 8 个以上,如果数组的长度还小于 64 的时候,则会扩容数组。如果数组的长度大于等于 64 了的话,在会将该节点的链表转换成树。
通过扩容数组的方式来把这些节点给分散开。然后将这些元素复制到扩容后的新的数组中,同一个链表中的元素通过 hash 值的数组长度位来区分,是还是放在原来的位置还是放到扩容的长度的相同位置去 。在扩容完成之后,如果某个节点的是树,同时现在该节点的个数又小于等于6个了,则会将该树转为链表。
get()
方法
取元素的时候,相对来说比较简单,通过计算 hash 来确定该元素在数组的哪个位置,然后在通过遍历链表或树来判断 key 和 key 的 hash,取出 value 值。
这篇文章并没有去对源码进行一行行的分析(因为太复杂了,目前对我来说稍微有点难度,并且暂时不想花太多时间在上面),只是参考一些大佬的文章并了解了一下 2 个版本的差异,而对于为什么要重写,个人觉得还是效率等问题,虽然代码量从 jdk7 的 1000 多行变为了 jdk8 的 6000 多行,并且 jdk8 中使用 Synchronized 而不是 ReentrantLock。jdk8 之前都说 synchronized 属于重量级锁,但 jdk8 做了优化之后性能并不会比 ReentrantLock 差,况且根据其结构对比,锁的粒度要减小,是单独对一个 Node 上锁。