前言:通过前面几篇文章的分析,我们知道HashMap是线程不安全的,Hashtable是线程安全的,但是很占资源,本文分析的ConcurrentHashMap也是线程安全的,所以我们就有必要去分析ConcurrentHashMap底层实现和保证线程安全性的机制。建议在看此文之前,去看下基于jdk1.8的HashMap的源码分析
本文还是会从成员属性、数据结构、构造方法、常用方法以及常见问题来分析ConcurrentHashMap的源码。以最简单的方式,来读懂最难的代码。
一:ConcurrentHashMap的具体实现
ConcurrentHashMap的数据结构和HashMap的数据结构是一致的,都是基于数组+链表+红黑树来实现的,这里就不去画图展示了,可以参考基于jdk1.8的HashMap的源码分析 下面就开始具体源码的分析。
1.1:ConcurrentHashMap的成员属性
//规定数组的最大容量,2的30次幂
private static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的初始容量、这个容量必须是2的次幂
private static final int DEFAULT_CAPACITY = 16;
//虚拟机限制的最大数组长度,在ArrayList中有说过
//数组作为一个对象,需要一定的内存存储对象头信息,对象头信息最大占用内存不可超过8字节。
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//并发量、为了兼容1.7的版本、
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//加载因子
private static final float LOAD_FACTOR = 0.75f;
//链表可能转换为红黑树的基本阈值(链表长度>=8)
static final int TREEIFY_THRESHOLD = 8;
//哈希表扩容后,如果发现红黑树节点数小于6,则退化为链表
static final int UNTREEIFY_THRESHOLD = 6;
//链表转换为红黑树的另一个条件,哈希表长度必须大于等于64才会转换,否则会扩容
static final int MIN_TREEIFY_CAPACITY = 64;
//就是每次进行转移的最小值
private static final int MIN_TRANSFER_STRIDE = 16;
//生成sizeCtl所使用的bit位数
private static int RESIZE_STAMP_BITS = 16;
//参与扩容的最大线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 记录sizeCtl中的大小所需要进行的偏移位数
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
//表示正在转移
static final int MOVED = -1; // hash for forwarding nodes
//表示已经转换成树
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
//获取可用的CPU个数
static final int NCPU = Runtime.getRuntime().availableProcessors();
//以Node<K,V>为元素的数组,长度必须为2的n次幂
transient volatile Node<K,V>[] table;
//下一个数组
private transient volatile Node<K,V>[] nextTable;
// baseCount为并发低时,直接使用cas设置成功的值
// 并发高,cas竞争失败,把值放在counterCells数组里面的counterCell里面
//所以map.size = baseCount + (每个counterCell里面的值累加)
private transient volatile long baseCount;
//控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
//当为负数时:-1代表正在初始化,-N就代表在扩容,-N表示有N-1个线程正在进行扩容操作
//当为0时:代表当时的table还没有被初始化
//当为正数时:表示初始化或者下一次进行扩容的大小
//默认值为0,当在初始化的时候指定了大小,这会将这个大小保存在sizeCtl中,大小为数组的0.75
private transient volatile int sizeCtl;
// 扩容下另一个表的索引
private transient volatile int transferIndex;
//通过cas实现的锁,0 无锁,1 有锁
private transient volatile int cellsBusy;
//counterCells数组,具体的值在每个counterCell里面
private transient volatile CounterCell[] counterCells;
//视图
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
上面的属性有的在HashMap一文分析中已经做了详细的解释,下面针对一些没出现的属性进行解析说明
-
private static final int MIN_TRANSFER_STRIDE = 16 这个属性是当数组在进行扩容操作的时候,transfer这个步骤存在多线程的情况,这个常量就表示一个线程执行transfer时,最少要对连续的16个hash桶进行transfer,也就是每次进行转移的最小值
-
private transient volatile long baseCount 这个属性是当并发量低的时候,baseCount的值就直接使用cas设置的值,当并发量比较大的时候,cas竞争失败,把值放在counterCells数组里面的counterCell里面,所以后面的map.size = baseCount + (每个counterCell里面的值累加)
-
private transient volatile int sizeCtl 这个属性时是控制标识符,用来控制table的初始化和扩容的操作,默认值为0,当在初始化的时候指定了大小,这会将这个大小保存在sizeCtl中,大小为数组的0.75,不同的值有不同的含义。当为负数时:-1代表正在初始化,-N就代表在扩容,-N表示有N-1个线程正在进行扩容操作;当为0时:代表当时的table还没有被初始化;当为正数时:表示初始化或者下一次进行扩容的大小;
1.2:ConcurrentHashMap中重要的内部类
1.2.1:Node<K,V> 节点,用来存储键值对,插入ConcurrentHashMap的结点,都会被包装成Node或者TreeNode
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString(){ return key + "=" + val; }
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
public final boolean equals(Object o) {
Object k, v, u; Map.Entry<?,?> e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
}
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
/**
* 支持map的get方法,通过hashcode和Key来获取一个node结点
*/
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
1.2.1:TreeNode<K,V>节点,用来存储键值对,插入ConcurrentHashMap的结点,都会被包装成Node或者TreeNode
当链表长度达到规定的量时,链表会转换成红黑树,当它并不是直接转换,而是将这些链表的节点包装成TreeNode放在TreeBin对象中,然后由TreeBin完成红黑树的转换。
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;
TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
Node<K,V> find(int h, Object k) {
return findTreeNode(h, k, null);
}
/**
* Returns the TreeNode (or null if not found) for the given key
* starting at given root.
*/
final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
if (k != null) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk; TreeNode<K,V> q;
TreeNode<K,V> pl = p.left, pr = p.right;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.findTreeNode(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
}
return null;
}
}
1.3:ConcurrentHashMap的构造方法
//方法一:空参构造
//创建一个带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencyLevel (16) 的ConcurrentHashMap
public ConcurrentHashMap() {
}
//方法二:带有初始容量的构造方法
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
//最大值或者最接近该容量的2的幂次方数
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
//方法三:创建一个带有指定初始容量、加载因子和默认 concurrencyLevel (1) 的ConcurrentHashMap
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
//方法四:创建一个带有指定初始容量、加载因子和并发级别的ConcurrentHashMap
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
注解:
- concurrencyLevel在JDK1.8中并不代表所允许的并发数,只是用来确定sizeCtl大小,在JDK1.8中的并发控制都是针对具体的桶而言,即有多少个桶就可以允许多少个并发数;
- initialCapacity的大小是一个最小的且大于等于initialCapacity大小的2的n次幂,如initialCapacity为10,则sizeCtl应该大于2的三次幂且小于等于2的四次幂就是16,若initialCapacity为16,则sizeCtl为16,若initialCapacity大小超过了允许的最大值,则sizeCtl为最大值。
- 在任何一个构造方法中,都没有对存储Map元素Node的table变量进行初始化。是因为ConcurrentHashMap采用的是延迟初始化,在第一次进行put操作的时候才会进行初始化。
1.4:ConcurrentHashMap的常用方法
1.4.1:ConcurrentHashMap常用方法之put()
put方法的基本流程:当第一次put元素的时候会判断table有没有初始化,没有初始化的话会去初始化table,接着通过计算hash值来确定放在数组的哪个位置,如果该位置为空,那么直接添加,如果该位置不为空,就需要取出该节点进行判断,此时,如果取出来的节点的哈希值是MOVED,那么表明此时正在进行扩容复制操作,就让当前线程也去复制元素。如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作,接下来就需要判断是链表还是红黑树,如果是链表则开始遍历,如果key的哈希值和key都相等,那么替换旧值并返回旧值,如果不等那么就添加到链表的结尾;如果是红黑树,那么就调用putTreeVal方法把这个元素添加到树中去。最后需要对链表添加之前的元素进行判断,如果达到了8个则调用treeifyBin方法来尝试将处的链表转为树,或者扩容数组;
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
//key和value都不能为空,否则抛出异常
if (key == null || value == null) throw new NullPointerException();
//得到key的hash值
int hash = spread(key.hashCode());
用来计数、在当前节点总共有多少个元素,用来控制扩容或者转移为红黑树
int binCount = 0;
//遍历table,死循环,直到插入成功
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//第一次put时,table没有初始化,初始化table
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//走到这里说明表不为空
//计算下标的位置,n为数组的长度,通过(n-1)&hash计算该放入的下标
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//如果该位置是空的,则通过cas的方式添加进去,此时并没有加锁
if (casTabAt(tab, i, null,
//插入新节点,下一个节点暂时为null
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//如果某个节点的哈希值是MOVED,那么说明这个正在进行数组的扩容和复制
else if ((fh = f.hash) == MOVED)
//当前线程也帮助其复制
tab = helpTransfer(tab, f);
else {
//走到这说明数组里有元素,要么添加元素、要么替换元素,增加完元素以后对链表进行判断是否转换成红黑树
V oldVal = null;
//加上同步锁
synchronized (f) {
// 找到table表下标为i的节点
if (tabAt(tab, i) == f) {
//该table表中该结点的hash值大于0,表明是链表 当转换为树之后,hash值为-2
if (fh >= 0) {
binCount = 1;
//遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
//要存的元素的hash,key和存储的位置的节点的相同时替换掉value
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
//如果不是同样的hash,同样的key的时候,则在链表结尾添加元素
Node<K,V> pred = e;
if ((e = e.next) == null) {
//如果当前节点的下一个节点为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;
}
}
}
}
//当在同一个节点的数目达到8个的时候,则扩张数组或将给节点的数据转为tree
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
//如果在8个以上的话,则会调用treeifyBin方法,来尝试转化为树,或者是扩容
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//计算插入的元素个数
addCount(1L, binCount);
//替换返回旧值,新增返回null
return null;
}
//tabAt()返回table数组中下标为i的结点
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
//casTabAt()用于比较table数组下标为i的结点是否为c,若为c,则用v交换操作。否则,不进行交换操作。
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
1.4.2:ConcurrentHashMap常用方法之put()中的initTable()
ConcurrentHashMap第一次进行put操作的时候会进行初始化,同时在扩容的时候也会执行初始化操作,这里把putVal()中调用的initTable()方法单独拎出来解析:
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//当table为null的时候,表明没有初始化,进入while循环
while ((tab = table) == null || tab.length == 0) {
//如果sizeCtl小于0的时候,表明别的线程正在初始化,通知线程调度器放弃对处理器的占用
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//SIZECTL表示当前对象的内存偏移量、sc表示期望值、-1表示要替换的值、设置为-1,接下来本线程对table进行操作
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// sc的值是否大于0,若是,则n为sc,否则,n为默认初始容量
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// sc为 n * 3/4
sc = n - (n >>> 2);
}
} finally {
//初始化后,sizeCtl长度为数组长度的3/4
sizeCtl = sc;
}
break;
}
}
return tab;
}
**注解:**从上面的代码可以看出再多线程的环境下,table的初始化主要是基于sizeCtl这个变量来决定谁去初始化table。没有拿到的线程将会一直去尝试获得这个共享变量,所以获得sizeCtl这个变量的线程在完成后需要设置回来,使得其他的线程可以进行接下来的操作。
1.4.3:ConcurrentHashMap常用方法之put()中的treeifyBin()
在上面的putVal方法中,我们可以看到如果binCount超过了界限值TREEIFY_THRESHOLD,会调用treeifyBin来把链表转化成红黑树,但是当table的长度未达到阈值时,会进行一次扩容操作。
//当链表长度大于等于8,数组长度小于64的时候,会将数组长度扩大一倍
//当链表长度大于等于8,数组长度大于等于64的时候,就会转换为红黑树
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//数组长度小于64、扩容
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
//数组中存在结点并且结点的hash值大于等于0
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
//对单个数组节点进行加锁
synchronized (b) {
//将该节点处的链表转为树
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
把Node组成的链表,转化为TreeNode的链表,头结点任然放在相同的位置
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
1.4.4:ConcurrentHashMap常用方法之put()中的tryPresize()
private final void tryPresize(int size) {
//计算扩容的size,如果传入的size大于等于最大容量的一半,那么就使用最大容量,否则tableSizeFor计算
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
//数组没有初始化、需要初始化数组
if (tab == null || (n = tab.length) == 0) {
//初始化一个大小为sizeCtrl和刚刚算出来的c中较大的一个大小的数组
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
//初始化的时候,设置sizeCtrl为-1,初始化完成之后把sizeCtrl设置为数组长度的3/4
sizeCtl = sc;
}
}
}
//当c小于等于sizeCtl或者数组长度大于最大长度的时候才会退出
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) {
int rs = resizeStamp(n);
//如果别的线程在在扩容,那么帮助它一起扩容
if (sc < 0) {
Node<K,V>[] nt;
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//transfer的线程数加一,此时sc表示在transfer工作的线程数
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
//transfer中将第一个参数的table中的元素,移动到第二个元素的table中去
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//当第二个参数为null的时候,会创建一个两倍大小的table
transfer(tab, null);
}
}
}
1.4.5:ConcurrentHashMap常用方法之put()中的transfer()
//数组扩容主要方法、将第一个参数的table中的元素,移动到第二个元素的table中去
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 这里的目的是让每个 CPU 处理的桶一样多,避免出现转移任务不均匀的现象,如果桶较少的话,默认一个 CPU(一个线程)处理 16 个桶
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
//MIN_TRANSFER_STRIDE默认16、每次进行转移的最小值
stride = MIN_TRANSFER_STRIDE; // subdivide range
//如果nextTab为null、那么就创建一个两倍长的table赋值给nextTab和nextTable
//nextTable由于一开始是null,但是当第一个线程走进来的时候就会扩大两倍
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
//扩容失败就让sizeCtl使用 int 最大值。
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
//表示转移时的下标、初始值是扩容之前的length
transferIndex = n;
}
int nextn = nextTab.length;
//创建一个标示节点、用来控制并发操作、当别的线程发现这个节点是 fwd 类型的节点,则跳过这个节点。
//如果当前节点为null或者已经被其他线程转移了,那么该节点就会被设置成fwd节点
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
/**
* 这个标示是为了记录当前线程下的数组转移的操作有没有完成
* 首次推进为true,为true就表明当前线程已经完成可以进行下一个数组(i--)的操作
*/
boolean advance = true;
//是否完成
boolean finishing = false; // to ensure sweep before committing nextTab
// 死循环、i 表示数组的下标、bound 表示当前线程可以处理的当前数组区间最小下标
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 控制 --i ,遍历原hash表中的节点
// 并且每个线程都会进入这里取得自己需要转移的数组的区间
while (advance) {
int nextIndex, nextBound;
/**
* 这个地方先解释一下,下面对 i 进行赋值的时候,i 拿到的值是当前线程可以处理的当前区间的最大下标
* 而 bound 表示当前线程可以处理的当前数组区间最小下标 举个例子:如果当前线程获取的区间是1到16
* 那么此时 i 的值就是 15 而此时bound就是 0 。
* 当 --i 大于等于 bound 时说明当前线程已经获取到需要转移的区间但是还没有全部完成转移任务,
* 所以进来把advance = false,等当前线程把任务切底完成后统一把 advance = finishing = true
* 在这个循环内只要进到条件里面都会有 advance = false 是因为任务领取成功还在等着下面的程序
* 去完成转换操作,等当前的任务完成后下面会修改这个状态,防止在没有成功处理一个驻数组的情况下却
* 进行了第二次的任务分配
*/
if (--i >= bound || finishing)
advance = false;
/**
* 这个循环第一次进来都不会走到上面,而是走到这里对nextIndex进行赋值操作,并且也不会进到判断里面
* 这里的赋值仅仅是为了获取最新的转移下标、其次就是当一个线程处理完自己的区间后、如果还有剩余区间
* 没有别的线程处理、它会再次获取区间。
* 如果走到 else if 里面说明数组转移的任务基本已经全部分配完了、没有区间需要分配了
* 就把 i 设置成 -1 为了下面判断 且把循环的条件改成 false 说明不需要分配任务、扩容结束了
*/
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//通过CAS计算得到的transferIndex、就是数组的长度减去已经分配的区间、剩下的就是没有处理的
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
// 这个值就是当前线程可以处理的区间的最小下标
bound = nextBound;
// 第一次给 i 赋值、表示当前线程可以处理的当前区间的最大下标
// nextIndex 是长度 减一 表示下标值
i = nextIndex - 1;
advance = false;
}
}
// i < 0 根据上面可以看出领取最后一段区间的线程扩容结束
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//已经完成所有节点复制了
if (finishing) {
//将nextTable置为null并将table指向nextTable
nextTable = null;
table = nextTab;
//设置sizeCtl为扩容后的0.75
sizeCtl = (n << 1) - (n >>> 1);
//跳出死循环
return;
}
//如果没有完成那么CAS更新扩容阈值,sizectl值减一,说明新加入一个线程参与到扩容操作
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 不相等,说明没结束,当前线程结束方法。
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//如果他们相等,说明没有线程在帮助他们扩容了、扩容结束了
finishing = advance = true;
// i 得到重新赋值、其实为了再次循环检查一下整张表
i = n; // recheck before commit
}
}
//数组中把null的元素设置为ForwardingNode节点(hash值为MOVED[-1])
// f 节点在这里进行赋值,表示老 tab i 下标位置的节点
else if ((f = tabAt(tab, i)) == null)
//设置成功后把advance改为true
advance = casTabAt(tab, i, null, fwd);
// 当f.hash == -1 表示遍历到了ForwardingNode节点,意味着该节点已经处理过了
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
/**
*进到这里面说明节点有实际值了,不为null,也不为ForwardingNode节点
*接下里做的就是对节点上锁,防止你复制链表的时候putVal向链表添加数据
*/
synchronized (f) {
//接下来就是节点的复制工作
if (tabAt(tab, i) == f) {
/**
* 这里会对老链表你进行拆解,一分为二、和HashMap中的扩容有点类似
* ln表示低位节点
* hn表示高位节点
*/
Node<K,V> ln, hn;
//节点的哈希值大于 0 的时候,表明是个链表节点、TreeBin 的 hash 是 -2
if (fh >= 0) {
//用节点的hash值和老数组长度进行 & 运算
int runBit = fh & n;
Node<K,V> lastRun = f;
//扩容的时候导致n的数量变了、这个地方的 & 运算其实是在判断链表的位置要不要改变
for (Node<K,V> p = f.next; p != null; p = p.next) {
//遍历这个链表,并对链表里面的元素进行 & 运算
int b = p.hash & n;
/**
* 这个地方会出现两种情况,第一是链表里面元素的hash值和头节点的hash值一样
* 第二是:链表里面元素的hash值和头节点的hash值不一样
* 这就是导致了一分为二
* 这里的解析有困惑的去看下笔者写的HashMap扩容的地方
*/
if (b != runBit) {
//用于下面判断 lastRun 该赋值给 ln 还是 hn。
runBit = b;
lastRun = p;
}
}
//runBit为0 就放在低位节点
if (runBit == 0) {
ln = lastRun;
hn = null;
}
//否则放在高位节点
else {
hn = lastRun;
ln = null;
}
//遍历,生成两个链表
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
//设置低位链表放在新链表的 i
setTabAt(nextTab, i, ln);
//设置高位链表,在原有长度上加 n
setTabAt(nextTab, i + n, hn);
// 将旧的链表设置成fwd节点
setTabAt(tab, i, fwd);
//当前链表复制完成
advance = true;
}
//如果是红黑树,也是相同的操作
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// 如果树的节点数小于等于 6,那么转成链表,反之,创建一个新的树
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
1.4.6:ConcurrentHashMap常用方法之put()中的helpTransfer()
//传进来的参数为成员变量 table 和 对应节点的 f
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
//新tab不为空 且 节点是标示的ForwardingNode节点
//且节点的nextTable(新 table)不是空 在下面的ForwardingNode方法中进行的初始化
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
//进来尝试一起扩容
//根据tab.length获取一个常量作为标识符
int rs = resizeStamp(tab.length);
//nextTab 和 tab 没有被修改 且 还在扩容中(sc = sizeCtl < 0)
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
//如果扩容结束或者达到最大值
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
//没有那就增加一个线程帮助其扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
1.4.7:ConcurrentHashMap常用方法之put()中的addCount()
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
至此,ConcurrentHashMap的put方法以及里面的扩容机制已经解读完毕,难点和需要注意的点都在代码前面的注释上。
1.4.8:ConcurrentHashMap常用方法之get()方法
/**
* get方法相对简单很多,通过key的哈希值去获取在数组中的位置 支持并发操作
* 如果该位置的key不仅是哈希值相等而且key的equal比对也一样,那么返回value
* 如果不等,那么遍历链表获取到那个值,没有返回 null
*/
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;
}
二:ConcurrentHashMap的常见问题
2.1:ConcurrentHashMap扩容过程中,读写操作能否访问到数据?
解析: 可以的。当数组在扩容的时候,会对当前操作节点进行判断,如果当前节点还没有被设置成fwd节点,那就可以进行读写操作,如果该节点已经被处理了,那么当前线程也会加入到扩容的操作中去。
2.2:ConcurrentHashMap在多线程线下如何保证正确性?
解析: 主要是通过 Synchronized 和 unsafe 以及 ForwardingNode 来实现的,在获取某个位置的节点的时候,通过使用unsafe 里面的方法达到并发安全的作用;当数组扩容的时候,通过设置线程处理的数组区间和设置 fwd 节点来完成并发操作;当需要对某个节点下的数据进行操作的时候,通过使用 Synchronized 的同步机制来锁定该位置的节点。
2.3:ConcurrentHashMap的get方法是否要加锁,为什么?
**解析:**不需要,get方法采用了unsafe方法,来保证线程安全。
2.4:ConcurrentHashMap和hashTable都是线程安全的,那他们之间的区别是哪些?
解析:
- Hashtable采用对象锁(synchronized修饰对象方法)来保证线程安全,也就是一个Hashtable对象只有一把锁,如果线程1拿了对象A的锁进行有synchronized修饰的put方法,其他线程是无法操作对象A中有synchronized修饰的方法的(如get方法、remove方法等),竞争激烈所以效率低下。而ConcurrentHashMap采用CAS + synchronized来保证并发安全性,且synchronized关键字不是用在方法上而是用在了具体的对象上,实现了更小粒度的锁;
- Hashtable采用的是数组 + 链表,当链表过长会影响查询效率,而ConcurrentHashMap采用数组 + 链表 + 红黑树,当链表长度超过某一个值,则将链表转成红黑树,提高查询效率。
至此,ConcurrentHashMap的源码研究告一段落,欢迎大家一起讨论学习。
了解更多干货,欢迎关注我的微信公众号:爪哇论剑