1. 存储结构
Java8 的 ConcurrentHashMap 是 Node 数组 + 链表 / 红黑树。当冲突链表达到一定长度时,链表会转换成红黑树。针对Node数组中的每个Node节点进行加锁保证线程安全.
2. 初始化 initTable
sizeCtl变量
当
sizeCtl
的值为-1
时:表示哈希表正在初始化。这通常是在创建ConcurrentHashMap
对象时的初始状态。在初始化过程中,只允许一个线程执行初始化操作,其他线程需要等待。当
sizeCtl
的值为负数,但不是-1
时:表示哈希表正在进行调整大小的操作。具体来说,它值为-1 - 线程数量,
其中线程数量表示参与调整大小操作的活动线程的数量. 在哈希表进行扩容或收缩时,多个线程可能会协同工作确保表的大小调整正确进行。当
sizeCtl
的值为0
时:表示哈希表还没有初始化,即在创建ConcurrentHashMap
对象时,还没有确定初始的哈希表大小。在这种情况下,通常使用默认的哈希表大小。当
sizeCtl
的值为正数时:表示哈希表已经初始化,并且它指示了下一次调整大小的元素数量。一旦哈希表中的元素数量达到sizeCtl
指定的值,就会触发哈希表的扩容操作。
SIZECTL变量
SIZECTL
是一个常量,它被设置为U.objectFieldOffset()
方法的返回值。这个常量的目的是获取ConcurrentHashMap
类中的sizeCtl
字段的偏移量。这个偏移量通常用于通过反射或Unsafe
类来访问对象中的字段,因为它可以帮助确定字段在对象内存中的位置。让我解释一下代码的含义:
ConcurrentHashMap.class
: 这是ConcurrentHashMap
类的Class
对象,用于表示该类的元数据。
"sizeCtl"
: 这是字段的名称,即ConcurrentHashMap
类中的sizeCtl
字段的名称。
U.objectFieldOffset
: 这是Unsafe
类的方法,它接受一个类和字段的名称,并返回表示该字段在对象内存中的偏移量。
SIZECTL = U.objectFieldOffset(...)
: 这行代码的目的是使用U.objectFieldOffset
方法获取ConcurrentHashMap
类中的sizeCtl
字段的偏移量,并将它赋值给SIZECTL
常量。这个常量可以在以后的代码中用于访问和操作sizeCtl
字段。
通过这种方式,程序可以绕过正常的访问控制机制来访问对象内部的字段,这在某些情况下是必要的,但要小心使用,因为它涉及到底层内存操作和可能会破坏对象封装性。这通常在特殊情况下,比如高性能数据结构的实现中才会使用。
initTable()方法源码
从源码中可以发现 ConcurrentHashMap
的初始化是通过自旋和 CAS 操作完成的。里面需要注意的是变量 sizeCtl
,它的值决定着当前的初始化状态。
- -1 说明正在初始化
- -N 说明有 N-1 个线程正在进行扩容
- 0 表示 table 初始化大小,如果 table 没有初始化
- >0 表示 table 扩容的阈值,如果 table 已经初始化。
3. put
用于将键值对添加到哈希表中
initTable()方法在 put()方法 中被使用。
执行在一开始没有Node节点数组时
-
根据 key 计算出 hashcode 。
-
判断是否需要进行初始化。
-
即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
-
如果当前位置的
hashcode == MOVED == -1
,则需要进行扩容。 -
如果都不满足,则利用 synchronized 锁写入数据。
-
如果数量大于
TREEIFY_THRESHOLD
则要执行树化方法,在treeifyBin
中会首先判断当前数组长度 ≥64 时才会将链表转换为红黑树。
代码:
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key 和 value 不能为空
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// f = 目标位置元素
Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值
if (tab == null || (n = tab.length) == 0)
// 数组桶为空,初始化数组桶(自旋+CAS)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 使用 synchronized 加锁加入节点
synchronized (f) {
if (tabAt(tab, i) == f) {
// 说明是链表
if (fh >= 0) {
binCount = 1;
// 循环加入新的或者覆盖节点
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
// 红黑树
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
4. get
get 流程比较简单,直接过一遍源码。
总结一下 get 过程:
- 根据 hash 值计算位置。
- 查找到指定位置,如果头节点就是要找的,直接返回它的 value.
- 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。
- 如果是链表,遍历查找之。
3. 总结
Java8 中的 ConcurrentHashMap
使用的 Synchronized
锁加 CAS 的机制。结构是 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。