4-3 ConcurrentHashMap(2021-11-11)
1 ConcurrentHashMap jdk1.8简介
- jdk1.8中ConcurrentHashMap的结构是:数组+链表+红黑树。
2 ConcurrentHashMap在jdk1.7和jdk1.8中的区别
-
JDK 1.7 使用数组+Segment+分段锁的方式实现。
- Segment继承自重入锁 ReentrantLock,并发度与 Segment 数量相等。
- ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。
-
JDK 1.8 采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作。并发控制使⽤synchronized 和 CAS 来操作。
- 可以理解为,synchronized 只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要 hash 不冲突,就不会产⽣并发,效率⼜提升 N 倍。
3 源码
3-1 成员变量
- sizeCtl,该字段控制**table(也被称作hash桶数组)**的初始化和扩容。sizeCtl为负数的时候,表示table初始化或者扩容。
private transient volatile int sizeCtl;
sizeCtl = -1 // 表示已经初始化。
sizeCtl = -(1+正在扩容的线程数)
- MAXIMUM_CAPACITY,table最大容量是2的30次方。
private static final int MAXIMUM_CAPACITY = 1 << 30;
- DEFAULT_CAPACITY,table默认初始化容量16,扩容总是2的n次方。
private static final int DEFAULT_CAPACITY = 16;
- LOAD_FACTOR,table的负载因子。当前已使用容量 >= 负载因子*总容量的时候,进行resize扩容。
private static final float LOAD_FACTOR = 0.75f;
- table,整个hash表的结构。也被称作hash桶数组。
transient volatile Node<K,V>[] table;
- TREEIFY_THRESHOLD,当桶内链表长度>=8时,会将链表转成红黑树。
static final int TREEIFY_THRESHOLD = 8;
- UNTREEIFY_THRESHOLD,当桶内node小于6时,红黑树会转成链表。
static final int UNTREEIFY_THRESHOLD = 6;
- MIN_TREEIFY_CAPACITY ,table的总容量,要大于64,桶内链表才转换为树形结构,否则当桶内链表长度>=8时会扩容。
static final int MIN_TREEIFY_CAPACITY = 64;
3-2 内部类
- Node
static class Node<K,V> implements Map.Entry<K,V> {
//hash =(-1为ForwardingNode表示正在扩容,-2为TreeBin表示桶内为红黑树,大于0表示桶内为链表。)
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
......
}
- TreeNode
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
}
- TreeBin
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // 写锁。
static final int WAITER = 2; // 等待写锁。
static final int READER = 4; // 读锁。
}
- ForwardingNode,在扩容时使用,实现了扩容时新表和旧表的连接。
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
}
当数组槽为空或已经完成数组槽的扩容后插入数组槽中告知其他线程。如果旧数组的一个hash桶中全部的节点都迁移到新数组中,旧数组就在这个hash桶中放置一个ForwardingNode。读操作或者迭代读时碰到ForwardingNode时,将操作转发到扩容后的新的table数组上去执行,写操作碰见它时,则尝试帮助扩容。
3-3 构造函数
public ConcurrentHashMap() { // 创建一个新的 map ,初始table size为16 。
}
public ConcurrentHashMap(int initialCapacity) { // 带参构造。
if (initialCapacity < 0) // 乱搞。
throw new IllegalArgumentException();
int cap = ( initialCapacity >= (MAXIMUM_CAPACITY >>> 1) ) ? MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1) );
//tableSizeFor方法得到一个大于负载因子入参且最接近2的N次方的数作为容量。
this.sizeCtl = cap; //设置sizeCtl的值等于初始化容量。未对table进行初始化,table的初始化要在第一次put的时候进行。
}
private static final int tableSizeFor(int c) {
int n = c - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
3-4 对table初始化
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) { // 如果table为空,进入while准备开始初始化。
if ((sc = sizeCtl) < 0)// 将sizeCtl赋值给sc。如果sizeCtl<0,线程等待。
Thread.yield(); // 线程等待。
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //如果能将sizeCtl设置为-1,则开始进行初始化操作。
try {
if ((tab = table) == null || tab.length == 0) {
//用户有指定初始化容量,就用用户指定的,否则用默认的16。
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
// 生成一个长度为16的Node数组,把引用给table。
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2); // 重新设置sizeCtl=数组长度 - (数组长度 >>>2)。可以理解成n-(n/2)。
}
} finally {
sizeCtl = sc;// 重新设置sizeCtl。
}
break;
}
}
return tab; // 返回新的tab。
}
3-5 put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;// 用来记录所在table数组中的桶中链表的个数,后面会用于判断是否链表过长需要转红黑树。
for (Node<K,V>[] tab = table;;) {// for循环,直到put成功插入数据才会跳出。
Node<K,V> f; // f=桶头节点 n=table的长度 i=在数组中的哪个下标 fh=头节点的hash值
int n, i, fh;
if (tab == null || (n = tab.length) == 0)// 如果table没有初始化:
tab = initTable();// 初始化table。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// 根据数组长度减1再对hash值取余得到在node数组中位于哪个下标。// 用tabAt获取数组中该下标的元素。
// 如果该元素为空,直接将put的值包装成Node用castabAt方法放入数组内这个下标的位置中
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) // 如果头结点hash值为-1,则为ForwardingNode结点,说明正再扩容。
tab = helpTransfer(tab, f);// 调用hlepTransfer帮助扩容。
else {// 否则锁住槽的头节点。
V oldVal = null;
synchronized (f) {// 锁桶的头节点。
if (tabAt(tab, i) == f) {// 双重锁检测,看在加锁之前,该桶的头节点是不是被改过了。
if (fh >= 0) { // 如果桶的头节点的hash值大于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; // 如果遇到节点hash值相同,key相同,看是否需要更新value。
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);// 就生成Node挂在链表尾部,该Node成为一个新的链尾。
break;
}
}
}
else if (f instanceof TreeBin) {// 如果桶的头节点是个TreeBin
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) { // 用红黑树的形式添加节点或者更新相同hash、key的值。
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);// 新增后,返回null。
return null;
}
3-6 get方法
public V get(Object key) {
Node<K,V>[] tab;
Node<K,V> e, p;
int n, eh; K ek; //临时。
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
3-7 replace方法
public V replace(K key, V value) { // 替换。
if (key == null || value == null) // 不能为空。
throw new NullPointerException();
return replaceNode(key, value, null);
}
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
validated = true;
for (Node<K,V> e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
4 ConcurrentHashMap和HashTable的区别
- ConcurrentHashMap不论1.7还是1.8,他的执行效率都比HashTable要高的多,主要原因还是因为Hashtable使用了一种全表加锁的方式。