在分析ConcurrentHashMap的实现原理之前,先来介绍一下hashmap以及hashtable的特点和可能存在的问题.
TreeMap
//todo
HashTable
hashTable,它是线程安全的,它在所有涉及到多线程操作的都加上了synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争一把锁,在多线程的环境下,它是安全的,但是无疑是效率低下的,尤其是在过去synchronized比较重的时候。
但是在jdk1.5之后,synchronized关键字的效率得到了很大提升,因此不确定HashTable的效率是否还像以前那样所说的低下。
ConcurrentHashMap
jdk1.7 基于分段锁
在jdk1.7之中,ConcurrentHashMap使用的是分段锁技术实现,把HashMap分割成若干个Segment,在put的时候需要锁住Segment,get时候不加锁,使用volatile来保证可见性。在concurrentHashMap中存放数据的是一个Node<K,V>的内部类,而存放所有数据的就是这些Node组合起来的一个table,它的定义如下:
/**
* The array of bins. Lazily initialized upon first insertion.
* Size is always a power of two. Accessed directly by iterators.
*/
transient volatile Node<K,V>[] table;
使用了volatile修饰,保证可见性。
当要统计全局时(比如size),首先会尝试多次计算modcount来确定,这几次尝试中,是否有其他线程进行了修改操作,如果没有,则直接返回size。如果有,则需要依次锁住所有的Segment来计算。下图是ConcurrentHashMap的一个结构图:
jdk1.8 基于CAS
jdk7中ConcurrentHashmap中,当长度过长碰撞会很频繁,链表的增改删查操作都会消耗很长的时间,影响性能,所以jdk8 中完全重写了concurrentHashmap,代码量从原来的1000多行变成了 6000多 行,实现上也和原来的分段式存储有很大的区别。在jdk1.8之中,完全摒弃了segment的实现,而是借鉴了Hashmap的实现,也采用了数组加链表加红黑树的实现,并且也是在链表长度大于8时转换为红黑树。其结构大致如下:
put时不是锁住segment而是锁住一个具体的node,减小锁粒度,提高了效率。除此之外,put时使用的一个重要的思想就是CAS思想. put方法的源代码如下:
/** Implementation for put and putIfAbsent */
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;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
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
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
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;
}
在for循环之前,他会根据key去计算一个int类型的hash值,计算的方式如下:
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
最关键的逻辑在for循环遍历整个table里面,有一个if,两个else if,一个else,一共四个处理分支我们一个一个来看。
第一个if的判断条件是: if (tab == null || (n = tab.length) == 0). tab就是指的意思是当前遍历的这个table,如果当前这个table为空或者这个table的长度是0,那么就先初始化这个table。初始化table调用的方法为initTable,代码如下:
/**
* Initializes table, using the size recorded in sizeCtl.
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
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);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
这里是使用了CAS来保证只有一个线程能够实例化成功table. 他的实现调用了来自于类sun.misc.Unsafe. 在这个Unsafe类中它是一个native方法。关于CAS的内容请参见另一篇博客。
第二个else if 的判断条件是: else if ((f = tabAt(tab, i = (n - 1) & hash)) == null).
注意,f已经在此处复制。这里的hash是一个int类型的值,它事实上就是put方法的参数key,更准确的说是是根据key计算出的hash值,这里判断的是根据这个key的hash值去当前table中找对应的元素,若找到的为空,则证明当前key在map里面是没有映射的。根据hash值去当前table中找元素的方法是tabAt,它的定义如下:
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);
}
这里获取数组中对应元素使用的方法也是来组Unsafe类的方法getObjectVolatile, 我们应该都知道,在数组中知道下标之后,可以直接使用table[index]就能获得对应的元素。那这里为什么要这么做呢?并且,该table已经是被volatile修饰了,这意味着它的任何改动应该都是立刻可的值得。暂且先放过这个问题留待之后回答。
那么现在就直接插入吗?这样想就错了,因为我们必须考虑并发,在这里若判断成立,则说明当前位置为空是第一次插入元素(这里在jdk源代码里的注释很有意思:// no lock when adding to empty bin),它则接着调用了一个方法:casTabAt(),这个方法的定义如下,这个方法的实现也是使用CAS的方式来进行put,与上面其实是一样的。使用了compareAndSwapObject方法插入Node节点。
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);
}
若CAS成功,则说明Node插入成功,则break,然后调用addCount(1L,binCount)方法检查当前容量是否需要扩容
若CAS失败,说明在这个插入之前其他线程提前插入了节点,则自旋重新尝试在这个位置插入节点。问题:这个自旋锁是如何实现的?目前暂时没有看到解释。
第三个else if 的判断条件是:((fh = f.hash) == MOVED)
注意,fh变量已经在此处赋值。MOVED的定义如下:
static final int MOVED = -1; // hash for forwarding nodes
这里的意思是若f的hash值为-1,说明当前f是一个ForwadingNode节点,有其他线程正在扩容,则一起进行扩容,调用方法helpTransfer
第四个else 就是除了上面三个判断条件的其他所有情况
其余情况把新的Node节点按链表或红黑树的方式插入到合适的位置,这个过程采用同步内置锁实现并发。具体代码如下:
V oldVal = null;
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;
}
}
可以看到在这里面首先是对f加上了同步锁(注意,f在第二个判断时,若不为null,则已经进行赋值),然后又分了两种情况,第一是普通链表时的put,第二是红黑树的put操作(若f是TreeBin类型节点,说明f是红黑树的节点)。在put操作里会根据是否已经有值决定是替换还是新增一个Node。
最后在put插座结束之后,判断binCount >= TREEIFY_THRESHOLD,即当节点数大于等于8时,就执行treeifyBin方法,把链表转换为红黑数结构。代码如下:
/**
* Replaces all linked nodes in bin at given index unless table is
* too small, in which case resizes instead.
*/
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
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);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
关于putIfAbsent方法,他的实现和put是一样的,只是如果(调用该方法时)key-value 已经存在,则返回那个 value 值。如果调用时 map 里没有找到 key 的 mapping,返回一个 null 值
扩容
当table容量不足的时候,即table的元素数量达到容量阈值sizeCtl,需要对table进行扩容。
整个扩容分为两部分:
- 构建一个nextTable,大小为table的两倍。
- 把table的数据复制到nextTable中。
代码大致如下:
private final void addCount(long x, int check) {
...
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();
}
}
}
图片来源于:https://segmentfault.com/p/1210000010020931/read