Q: ConcurrentHashMap需要满足什么样的需求(也就是解决了什么样的问题)
A: ConcurrentHashMap首先是一个map,所以有基本的put, get方法,当然也会有size方法等,但是put和get是最常用也是最重要的。ConcurrentHashMap和普通Map不一样的地方是,它解决了多个线程同时进行put或者get的时,可能带来的临界区冲突(race condition)的问题。考虑以下几个场景:
- Map的有个键值对为1->"one",线程A在将其修改为1->"two",线程B在线程A后开始start,但是线程B在线程A进行真正写入前读到了尚未修改的1->"one",而不是原本希望的1->"two"。此时,我们期望的是线程A开始进行put操作之后,线程B再进行get,得到的就是更新之后的值。也就是说put操作是一个原子操作。
- 一个空map,线程A和B同时对其进行写入,线程A写入0到10000的偶数,线程B写入1到10000的奇数,最终结果map的size小于10000。原因是在HashTable的实现中,内部有一个成员变量是size,每一次的put的时候会进行++size,而这并不是一个原子操作,所以可能会出现A和B先后拿到size的旧值,然后分别在size上加1。同时这个size会用来判断HashTable中用于存放键值对的数组是否需要扩容。如果size比实际put进去的元素少,那么扩容就不会及时进行。往一个已经占满的键值对数组里面进行put,新的值会链接在数组所在元素的后面,以单链表的形式。
K: put, get, size, 多线程访问不会出错
R:
Q: 如何满足put, get?如何满足多线程时put及get不出错?
Q: ConcurrentHashMap如何实现put功能?
A: 要实现put,首先要有一个ConcurrentHashMap。即需要创建一个HashMap。其创建代码:
ConcurrentHashMap map = new ConcurrentHashMap<>();
内部实现为:
public ConcurrentHashMap() {
}
可以看到其实什么都没有做。还有另外一种方式是:
ConcurrentHashMap map = new ConcurrentHashMap<>(2);
//实现
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
这里也并没有进行具体的创建,只是对成员变量sizeCtl进行了赋值。
那么,具体的创建是在什么地方呢?
map.put("one", "1");
ConcurrentHashMap会在第一次进行put的时候判断是否已经创建了具体的table,如果没有就进行创建。
...
for (Node[] tab = table;;) {
Node f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
...
其中initTable,就是具体创建的地方:
/**
* Initializes table, using the size recorded in sizeCtl.
*/
private final Node[] initTable() {
Node[] tab; int sc;
while ((tab = table) == null || tab.length == 0) { // table为this.table
if ((sc = sizeCtl) < 0) // 初始化时,sizeCtl的值为0或者正数
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 将SIZECTL也就是sizeCtl的值赋值为-1,此时如果有别的线程进入的话,compareAndSwapInt会返回false,然后由于sizeCtl是volatile,此时该线程再次访问sizeCtl时就是最新的值-1,就执行上面的if代码,该线程进行yield()
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node[] nt = (Node[])new Node,?>[n]; // 此处进行HashMap的创建,也就是创建一个Node的数组
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc; 将sizeCtl的值赋为sc,也就是Node数组的长度
}
break;
}
}
return tab;
}
实际上,ConcurrentHashMap是使用一个Node
static class Node implements Map.Entry {
final int hash;
final K key;
volatile V val;
volatile Node next;
Node(int hash, K key, V val, Node next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
...
其实是一个单向链表,next指向了下一个节点。
上面的compareAndSwapInt是Java的CAS操作,具体实现根据各个操作系统而定。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
java中表明为native的方法表示是由native的代码来实现的。也就是各个操作系统不同可能不一样。
上面的initTable的代码中,有一个成员变量sizeCtl很重要,后面的分析都会遇到。这里先看看它的具体实现:
/**
* Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
private transient volatile int sizeCtl;
注意上面的英文注释,翻译一下就是:
- sizeCtl主要用于控制Table的初始化和resize。
- 当sizeCtl的值为负数的时候:-1表示在初始化,其他负数表示在resize
- 当Table没有被初始化,还是空的时候,sizeCtl保存了用来初始化Node
数组的初始长度,0表示使用默认初始长度。
那么SIZECTL又是什么呢?
private static final long SIZECTL;
static {
try {
U = sun.misc.Unsafe.getUnsafe();
Class> k = ConcurrentHashMap.class;
SIZECTL = U.objectFieldOffset
(k.getDeclaredField("sizeCtl"));
...
} catch (Exception e) {
throw new Error(e);
}
}
上面的代码可以看出SIZECTL只是sizeCtl属性对于当前对象的一个offset(偏移量),而compareAndSwapInt函数的第二个参数就是偏移量。所以U.compareAndSwapInt(this, SIZECTL, sc, -1)是将this对象偏移量为SIZECTL的值修改为-1,前提是当前值是sc。this对象偏移量为SIZECTL的位置就是sizeCtl。
那么,到这里,我们大致知道ConcurrentHashMap是怎么进行初始化的了。接下来是如何进行put的。put的时候有几种场景:
- put的key不存在,Node
依然有足够的余量(未使用空间)。此时直接进行插入即可。 - put的key存在,此时新put的值应当覆盖原有的值
- put的key不存在,但是跟已经存在的key有hash冲突,此时新的值应当放在同一个数组位置的Node链表中
- put的key不存在,Node
余量不足,需要扩容。
我们先来看看代码:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); //计算hash码
int binCount = 0;
for (Node[] tab = table;;) { //将成员变量table赋值给tab
Node f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); //初始化table
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //根据hash获取table中的值,如果为NULL,则继续。注意此时新取出的的值被赋给了变量f
if (casTabAt(tab, i, null,
new Node(hash, key, value, null))) //通过CAS设置table中位置为i的地方,设置为一个新的Node,此处对应场景1
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED) // 将f.hash赋给fh,并判断是否为MOVED,MOVED的值为-1
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) { // 对当前Node进行加锁
if (tabAt(tab, i) == f) { // 获取table上位置为i的对象,判断是否等于f(引用相等)
if (fh >= 0) {
binCount = 1;
for (Node e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) { //hash是根据传入的key计算出的hash,而e.hash是从Table里面位置为i的地方拿到的对象的hash值,如果二者相等,表明有hash冲突,此时如果key相等,则将新的val覆盖旧的val,此处对应场景2
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break; //此处有break
}
Node pred = e;
if ((e = e.next) == null) { // 如果上面的if没有执行,那么将e.next赋给e,也就是到链表中的下一个节点,如果此节点不为null,则继续进行循环,直到到达链表的尾部,此时if成立,将新的Node添加到链表的尾部,此处对应场景3。注意此时binCount实际上记录了链表有多少个节点
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
if ((p = ((TreeBin)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD) //如果大于TREEIFY_THRESHOLD,就进行Tree化,也就是使用Tree而不是链表来存储同一个hash值下的数据。此时的Node数组中存储的是TreeBin,由于TreeBin extends Node,所以这是可以的。一个TreeBin里面又包含了TreeNode,同时也包含了树的根节点。TreeNode extends Node,同时具有parent, left, right, prev四个指向其他TreeNode的指针。
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); // 这个地方对应着场景4,对Node[]进行扩容,具体实现请看下个问题
return null;
}
注意,上面代码中当binCount >= TREEIFY_THRESHOLD
的时候,会对单链表数组元素进行树化,单链表会变成一颗红黑树。具体过程就不在此多做描述了。
K: Node
R:
Q: 如何实现多线程的支持?
A: 在initTable的时候通过变量sizeCtl来实现只有一个线程在进行初始化,具体实现方式是:
- volatile,sizeCtl是一个被修饰为volatile的成员变量,也就是说线程对该值的修改发生于线程对其的读取之前。同时,每次读取sizeCtl都是最新的值。
- compareAndSwapInt。这个函数是一个CAS操作,是一个原子操作。当线程发现对sizeCtl的赋值无法进行时,会返回false,此时就不会进行初始化。重新回到while循环之后,另一个线程将sizeCtl赋值为-1,此时该线程只能yield()了。
关键代码:
private transient volatile int sizeCtl;
...
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
...
在进行put的时候:
- 获取Node,使用tabAt函数。tabAt实际就是对值的volatile读
- 添加新Node,使用casTabAt函数。casTabAt就是compareAndSwapObject的一层封装
- 更新key对应的value或者处理Hash冲突,使用synchronized(f),同时在里面也使用了tabAt
static final Node tabAt(Node[] tab, int i) {
return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
static final boolean casTabAt(Node[] tab, int i,
Node c, Node v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
static final void setTabAt(Node[] tab, int i, Node v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
K: volatile读,CAS写。volatile读写。
Q: 如何对Node
A: ConcurrentHashMap的扩容操作,主要就是将原来的Node
要完成这个目标,就有几个问题:如何构建新的更大容量的Node
上源码:
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[] tab, nt; int n, sc; // 创建tab, nt变量
while (s >= (long)(sc = sizeCtl) && (tab = table) != null && // 将tab变量赋值为类成员变量table,即具体存储值的数组
(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); // 传入tab变量,进行扩容
s = sumCount();
}
}
}
具体扩容方法:
/**
* Moves and/or copies the nodes in each bin to new table. See
* above for explanation.
*/
private final void transfer(Node[] tab, Node[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating,初始化,上面addCount函数中调用时一开始传入的即为NULL
try {
@SuppressWarnings("unchecked")
Node[] nt = (Node[])new Node,?>[n << 1]; //使用新的长度创建数组,n上面赋值为tab.length,此处的nt是nextTab的缩写
nextTab = nt; // 将新数组引用赋予参数nextTab
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab; // 将刚刚执行过赋值的nextTab赋给成员变量nextTable
transferIndex = n;
}
int nextn = nextTab.length; //nextn即表示nextTable的length,在整个代码中,n一般都表示length
ForwardingNode fwd = new ForwardingNode(nextTab); //新的nextTable放入ForwardingNode中
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1; //nextIndex = transferIndex = n = tab.length,所以此时i=tab.length-1
advance = false; //跳出while循环
}
}
if (i < 0 || i >= n || i + n >= nextn) { //第一次时i = n -1,所以没有一个条件满足
int sc;
if (finishing) { // 扩容结束,将nextTab赋给table,返回
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null) // 如果原始数组的n-1没有元素,则放入一个ForwardingNode
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED) // hash值为MOVED的Node就是一个ForwardingNode,如果当前节点是一个ForwardingNode,则表明已经被处理过了。这里是多个线程同时处理扩容的关键
advance = true; // already processed
else {
synchronized (f) { // 加锁
if (tabAt(tab, i) == f) { // f在往上数第二个else if的时候被赋值为tabAt(tab,i),此时为加锁后再次确认,i位置的Node没有被更改,如果不成立,则下面的代码不执行
Node ln, hn;
if (fh >= 0) { //表明f不是一个特殊节点,比如-1就是MOVED,表示是一个ForwradingNode,-2表示TreeBin,-3为Reserved,为保留值(当前没有使用)
int runBit = fh & n;
Node lastRun = f;
for (Node p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node 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(ph, pk, pv, ln);
else
hn = new Node(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln); //这里开始进行赋值,将ln赋给nextTab的i位置
setTabAt(nextTab, i + n, hn);//将hn赋给nextTab的i+n位置
setTabAt(tab, i, fwd); //将原来的tab的i位置赋上一个ForwardingNode
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin t = (TreeBin)f;
TreeNode lo = null, loTail = null;
TreeNode hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode p = new TreeNode
(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;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}