ConcurrentHashMap详解
关于JDK1.8下的ConcurrentHashMap
一、ConcurrentHashMap的创建
ConcurrentHashMap
的创建是通过New的方式生成的。
ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap();
实际是一个无参构造。
public ConcurrentHashMap() {
}
二、ConcurrentHashMap的put()方法
ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap();
concurrentHashMap.put("key","value");
这里的实际调用为putVal(key, value, false)
,所以我们只关注putVal()方法即可。
public V put(K key, V value) {
return putVal(key, value, false);
}
下面我们分析下putVal(key, value, false)
。
- 首先第一步便是判断
key
和value
是否存在为空的情况,如果存在直接抛出空指针异常(NullPointerException
)快速失败。 - 获取
key
的hashCode
,通过key
的hashCode
右移16
位后的值与原key
的hashCode
进行或
运算,并且和HASH_BITS = 0x7fffffff
进行与
运算得到最终的hashCode
值。这样做的目的是为了减少hash
碰撞。 for (Node<K,V>[] tab = table;;)
对Node
数组进行循环,此处相当于while (true)
,Node<K,V> f
为当前索引位置的数据、n
为数组长度、i
为数据要存储的索引位置。- 判断
tab
是否为空,以及tab
大小是否为0
,首次进入为null
,执行初始化initTable()
方法。首先先判断tab
是否为nul
l或者tab
长度是否为0
,然后判断sizeCtl
是否<0
,<0
表示正在有线程进行初始化。Thread.yield();
线程调度器该线程让出CPU
,这里我们的sizeCtl
为0
,表示还没有进行初始化。通过CAS
修改sizeCtlU.compareAndSwapInt(this, SIZECTL, sc, -1)
状态为-1
,表示正在进行初始化。CAS
修改完成后返回true
。再次判断tab
是否为空,以及tab
大小是否为0
,这里为双重保障。为了防止其他线程以及完成了初始化。然后进行判断int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
,此处sc
为0
,返回默认值16(DEFAULT_CAPACITY)
。创建Node
数组进行长度初始化,初始化长度为16
。最后计算sc = n - (n >>> 2);
扩容阈值。在finally
中进行赋值扩容阈值。break;
退出循环,返回初始化好的Node
数组。 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null)
去判断当前索引下标在tab
数组中是否为空,为空则通过CAS
向对应tab
中的i
位置增加一个Node
,Node
里面包含了key
的HashCode
以及key
和value
。增加成功后返回。else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f);
如果f.hash == MOVED
说明该数组正在有线程进行扩容,需要帮忙进行扩容。helpTransfer(tab, f)
。- 如果没有其他线程在进行扩容的话,使用
synchronized
锁住当前Node
节点,if (tabAt(tab, i) == f)
如果当前i
索引位置下的Node
等于此次操作的Node
,if (fh >= 0)
判断当前是否是链表。binCount = 1;
初始化链表遍历头部,从1
开始遍历。进行for
循环,如果Node
节点存储的HashCode
和当前要存入的key
的hashCode
相同,并且两个Key
在经过==
或者equals
发现两者有一个相等。那么进行value
值得覆盖。此处因注意onlyIfAbsent
,只有当onlyIfAbsent
为false
才进行覆盖。如果不相等则判断当前Node
节点的下个节点是否为空,为空则重新创建Node
节点,并将该Node
节点存放在当前Node
节点的Next
上。如果当前Node的Next不为空,那么继续循环直到,找到Next为空的Node节点,然后进行处理。 - 如果不是
链表
代表是红黑树
,走红黑树
处理逻辑。 - 通过
binCount
判断是否需要进行扩容。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 首先先判断`tab`是否为`nul`l或者`tab`长度是否为`0`
while ((tab = table) == null || tab.length == 0) {
// 然后判断`sizeCtl`是否`<0`,`<0`表示正在有线程进行初始化。`Thread.yield();`线程调度器该线程让出`CPU`,这里我们的`sizeCtl`为`0`,表示还没有进行初始化。
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// 通过`CAS`修改`sizeCtlU.compareAndSwapInt(this, SIZECTL, sc, -1)`状态为`-1`,表示正在进行初始化。`CAS`修改完成后返回`true`。
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 再次判断`tab`是否为空,以及`tab`大小是否为`0`,这里为双重保障。为了防止其他线程以及完成了初始化。
if ((tab = table) == null || tab.length == 0) {
// 然后进行判断`int n = (sc > 0) ? sc : DEFAULT_CAPACITY;`,此处`sc`为`0`,返回默认值`16(DEFAULT_CAPACITY)`。
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 最后计算`sc = n - (n >>> 2);`扩容阈值
sc = n - (n >>> 2);
}
} finally {
// 在`finally`中进行赋值扩容阈值。
sizeCtl = sc;
}
// 退出循环
break;
}
}
//返回初始化好的`Node`数组。
return tab;
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 1. 首先第一步便是判断`key`和`value`是否存在为空的情况,如果存在直接抛出空指针异常(`NullPointerException`)快速失败。
if (key == null || value == null) throw new NullPointerException();
// 2. 获取`key`的`hashCode`,通过`key`的`hashCode`右移`16`位后的值与原`key`的`hashCode`进行`或`运算,
//并且和`HASH_BITS = 0x7fffffff`进行`与`运算得到最终的`hashCode`值。这样做的目的是为了减少`hash`碰撞。
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 判断`tab`是否为空,以及`tab`大小是否为`0`,首次进入为`null`,执行初始化`initTable()`方法。
//首先先判断`tab`是否为`nul`l或者`tab`长度是否为`0`,然后判断`sizeCtl`是否`<0`,`<0`表示正在有线程进行初始化。`Thread.yield();`线程调度器该线程让出`CPU`,这里我们的`sizeCtl`为`0`,表示还没有进行初始化。
//通过`CAS`修改`sizeCtlU.compareAndSwapInt(this, SIZECTL, sc,-1)`状态为`-1`,表示正在进行初始化。
//`CAS`修改完成后返回`true`。再次判断`tab`是否为空,以及`tab`大小是否为`0`,这里为双重保障。为了防止其他线程以及完成了初始化。
//然后进行判断`int n = (sc > 0) ? sc : DEFAULT_CAPACITY;`,此处`sc`为`0`,返回默认值`16(DEFAULT_CAPACITY)`。
//创建`Node`数组进行长度初始化,初始化长度为`16`。最后计算`sc
= n - (n >>> 2);`扩容阈值。在`finally`中进行赋值扩容阈值。`break;`退出循环,返回初始化好的`Node`数组。
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// `else if ((f = tabAt(tab, i = (n - 1) & hash)) == null)` 去判断当前索引下标在`tab`数组中是否为空,
//为空则通过`CAS`向对应`tab`中的`i`位置增加一个`Node` ,`Node`里面包含了`key`的`HashCode`以及`key`和`value`。增加成功后返回。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 如果f.hash == MOVED说明该数组正在有线程进行扩容,需要帮忙进行扩容。
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 如果没有其他线程在进行扩容的话,使用`synchronized`锁住当前`Node`节点
synchronized (f) {
//`if (tabAt(tab, i) == f)` 如果当前`i`索引位置下的`Node`等于此次操作的`Node`
if (tabAt(tab, i) == f) {
// `if (fh >= 0)`判断当前是否是链表。
if (fh >= 0) {
//`binCount = 1;`初始化链表遍历头部,从`1`开始遍历
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//进行`for`循环,如果`Node`节点存储的`HashCode`和当前要存入的`key`的`hashCode`相同,
//并且两个`Key`在经过`==`或者`equals`发现两者有一个相等
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
// 此处因注意`onlyIfAbsent`,只有当`onlyIfAbsent`为`false`才进行覆盖。
if (!onlyIfAbsent)
e.val = value;
break;
}
// 如果不相等则判断当前`Node`节点的下个节点是否为空,为空则重新创建`Node`节点,
//并将该`Node`节点存放在当前`Node`节点的`Next`上。如果当前Node的Next不为空,那么继续循环直到,找到Next为空的Node节点,然后进行处理。
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;
}
三、ConcurrentHashMap的红黑树插入putTreeVal方法
- 这里红黑树插入使用到了
TreeNode
,如果为空需要进行初始化,红黑树转换时第一次需要初始化TreeNode
阶段,初始化调用有参构造将HashCode
和Key
和Value
进行存储到TreeNode
中,然后跳出循环。 - 当前红黑树节点的的
hash
值和当前key
的哈希值做大小判断,大于则dir
置为-1
,dir
是为了进行左插入和右插入判断,小于则dir
置为1
,dir
是为了进行左插入和右插入判断。 - 如果当前节点的
key
,==
或者equals
要存入的key
直接返回当前节点。 - 如果
Class
为空并且,并且comparableClassFor == null
comparableClassFor
作用是判断key
是否实现了自定义排序接口Comparable
,如果没有实现就返回空,如果实现了就判断是否是String
类型,如果是String
就直接返回String.Class
。如果不是就通过class.getGenericInterfaces
获取当前key
的类实现的接口。然后遍历实现的接口,判断接口是否为ParameterizedType
并且,ParameterizedType
的RawType
是否为Comparable
接口,并且泛型类型不为空返回当前key
的Class
对象。 compareComparables
如果没有实现Comparable
接口就不走排序,否则走Comparable
排序compareTo
通过value
进行排序。- 首次进来为
false
触发下面if处理,进行初始化。判断当前节点得左节点是否为空,并且获取当前节点得左节点判断是否当前key
得hashCode
是否小于当前节点的左节点hashcode
,或者小于右节点,如果存在key
重复的现象则返回对应TreeNode
节点。 - 不存在
key
重复的现象走tieBreakOrder
,判断当前节点的key
和需要保存的key
在hashcode
相同的情况下通过排序进行比较。相等则返回0
。 dir
小于等于0则获取当前节点的左分支,否则获取右分支。当分支为空时新建TreeNode
节点,根据dir
来选择插入右分支还是左分支。- 小于等于
0
插入左分支,大于0
插入右分支。判断上级节点是否是否为红节点,首次红黑树创建red
为黑节点为false
。这里如果为黑节点那么对应子节点为红节点。如果为红节点需要进行加锁操作 。通过cas
进行加锁操作,这里锁住的是当前ConcurrentHashMap
对象。红黑树插入节点后,需要重新平衡balanceInsertion
返回重新平衡后的根节点。
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
//这里红黑树插入使用到了`TreeNode`,如果为空需要进行初始化,
//红黑树转换时第一次需要初始化`TreeNode`阶段,
//初始化调用有参构造将`HashCode`和`Key`和`Value`进行存储到`TreeNode`中,然后跳出循环。
if (p == null) {
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
// 当前红黑树节点的的hash值和当前key的哈希值做大小判断
else if ((ph = p.hash) > h)
// 大于则dir置为-1,dir是为了进行左插入和右插入判断
dir = -1;
else if (ph < h)
// 小于则dir置为1,dir是为了进行左插入和右插入判断
dir = 1;
// 如果当前节点的key,==或者equals要存入的key直接返回当前节点。
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
// 如果Class为空并且,并且comparableClassFor == null,
else if ((kc == null &&
//comparableClassFor作用是判断key是否实现了自定义排序接口Comparable,
//如果没有实现就返回空,如果实现了就判断是否是String类型,
//如果是String就直接返回String.Class。
//如果不是就通过class.getGenericInterfaces获取当前key的类实现的接口。
//然后遍历实现的接口,判断接口是否为ParameterizedType并且,
//ParameterizedType的RawType是否为Comparable接口,
//并且泛型类型不为空返回当前key的Class对象。
(kc = comparableClassFor(k)) == null) ||
//compareComparables如果没有实现Comparable接口就不走排序,
//否则走Comparable排序compareTo通过value进行排序。
(dir = compareComparables(kc, k, pk)) == 0) {
// 首次进来为false触发下面if处理,进行初始化。
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
// 判断当前节点得左节点是否为空,并且获取当前节点得左节点判断是否当前key得hashCode是否小于当前节点的左节点hashcode,或者小于右节点,如果存在key重复的现象则返回对应TreeNode节点
if (((ch = p.left) != null &&
(q = ch.findTreeNode(h, k, kc)) != null) ||
// 判断当前节点得右节点是否为空,并且
((ch = p.right) != null &&
(q = ch.findTreeNode(h, k, kc)) != null))
return q;
}
// 不存在key重复的现象走这里,判断当前节点的key和需要保存的key在hashcode相同的情况下通过排序进行比较。相等则返回0,
dir = tieBreakOrder(k, pk);
}
// dir小于等于0则获取当前节点的左分支,否则获取右分支。当分支为空时新建TreeNode节点,根据dir来选择插入右分支还是左分支。
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
TreeNode<K,V> x, f = first;
first = x = new TreeNode<K,V>(h, k, v, f, xp);
if (f != null)
f.prev = x;
// 小于等于0插入左分支
if (dir <= 0)
xp.left = x;
else
// 大于0插入右分支
xp.right = x;
// 判断上级节点是否是否为红节点,首次红黑树创建red为黑节点为false,
// 这里如果为黑节点那么对应子节点为红节点。
if (!xp.red)
x.red = true;
// 如果为红节点需要进行加锁操作
else {
// 通过cas进行加锁操作,这里锁住的是当前ConcurrentHashMap对象
lockRoot();
try {
// 红黑树插入节点后,需要重新平衡
root = balanceInsertion(root, x);
} finally {
// 释放锁
unlockRoot();
}
}
break;
}
}
// 检查红黑树的平衡性
assert checkInvariants(root);
return null;
}
四、ConcurrentHashMap的get()方法
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
tab
为Node数组、e
为需要获取到的Node
节点、n
为Node
数组长度、eh
为Key
的hashCode
,ek
为Node
的key
。int h = spread(key.hashCode());
获取key
的hashCode
,通过key
的hashCode
右移16
位后的值与原key
的hashCode
进行或
运算,并且和HASH_BITS = 0x7fffffff
进行与
运算得到最终的hashCode
值。这样做的目的是为了减少hash
碰撞。- 判断
Node
数组是否为Null
,或者Node
数组长度是否大于0
,为Null
或者小于0
,表示Node
未被初始化或者Node
数组没有数据。并且根据数组长度-1
和key
的hashCode
进行与
运算,去Node
数组中寻找是否存在该数据。不存在直接返回Null
。 - 如果获取出来对应的
Node
节点的HashCode
和当前出来Key
的HashCode
一致,并且Node
节点中的Key
和当前查询的Key
,==
或者equals
判断一致那么直接返回Node
中存储的Value
值。 - 如果不相等则判断当前
Node
中存储的HashCode
是否为-1
,-1
表示为红黑树
代表,需要进入红黑树
查询。红黑树
查找也为while
查找,通过遍历Node
节点的Next
节点进行Hash
、==
、equals
判断。 - 如果经过
Node
数组头节点和红黑树
都没有查找到,那么就需要进行Node
链表的遍历了,如果Node
节点的Next
不为空进行While
循环,通过HashCode
和==
、equals
进行判断,判断成立返回对应的Node
节点的Value
值。
public V get(Object key) {
//`Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;`
//`tab`为Node数组、`e`为需要获取到的`Node`节点、
//`n`为`Node`数组长度、`eh`为`Key`的`hashCode`,`ek`为`Node`的`key`。
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//` int h = spread(key.hashCode());`获取`key`的`hashCode`,
//通过`key`的`hashCode`右移`16`位后的值与原`key`的`hashCode`进行`或`运算,
//并且和`HASH_BITS = 0x7fffffff`进行`与`运算得到最终的`hashCode`值。
//这样做的目的是为了减少`hash`碰撞。
int h = spread(key.hashCode());
//判断`Node`数组是否为`Null`,或者`Node`数组长度是否大于`0`,
//为`Null`或者小于`0`,表示`Node`未被初始化或者`Node`数组没有数据。
//并且根据数组长度`-1`和`key`的`hashCode`进行`与`运算,去`Node`数组中寻找是否存在该数据。
//不存在直接返回`Null`。
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果获取出来对应的Node节点的HashCode和当前出来Key的HashCode一致,
//并且Node节点中的Key和当前查询的Key,==或者equals判断一致那么直接返回Node中存储的Value值。
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 如果不相等则判断当前Node中存储的HashCode是否为-1,-1表示为红黑树代表,需要进入红黑树查询。
// 红黑树查找也为while查找,通过遍历Node节点的Next节点进行Hash、==、equals判断。
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 如果经过Node数组头节点和红黑树都没有查找到,那么就需要进行Node链表的遍历了,
//如果Node节点的Next不为空进行While循环,通过HashCode和==、equals进行判断,
//判断成立返回对应的Node节点的Value值。
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
由于时间原因扩容还未分析,请耐心等待。感谢观看。