今天来看一下ConcurrentHashMap,网上看到很多都是说 它与HashMap 很像,像到什么程度呢?区别仅在于ConcurrentHashMap 在put时加了synchronized 关键字,所以多线程环境下,ConcurrentHashMap是安全的,那么接下来我们来看一下,底层是如何实现的,如果还不了解HashMap可以参考另一篇博文HashMap源码解析。
按照惯例,先来看一下定义的全局变量及含义:
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
// 最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认数组容量
private static final int DEFAULT_CAPACITY = 16;
// 最大数组容量
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 扩容负载因子
private static final float LOAD_FACTOR = 0.75f;
// 树化阀值
static final int TREEIFY_THRESHOLD = 8;
// 反树化阀值
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树化容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 最小转移步幅
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;
// 重置最大值
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
}
从全局变量我们可以看到,里面有一部分变量都是在HashMap中见到过的,并且值是相同的,由此我们可以简单的推断,初始化容量大小、扩容条件以及树化、反树化条件都是一致的,由此可以看出它与HashMap 还是很相似的,下面再来进一步的深入。
一、构造函数
// 无参构造
public ConcurrentHashMap() {}
// 初始化容量构造
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;
}
// Map构造(将其他Map转化为ConcurrentHashMap)
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
// 初始化容量及负载因子
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
// 初始化容量、负载因子及并发级别
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;
}
可以看到,构造函数还有比较多的,能够满足我们的大部分的使用场景。以下分析基于无参构造。
二、put() 方法
这个方法应该都是非常熟悉了,如果不熟悉只能说明你从没用过Map……
map.put(K key,V value);
看一下底层是如何实现的:
public V put(K key, V value) {
return putVal(key, value, false);
}
进入到put方法中,发现实际调用的是putVal方法,继续深入(由于该方法代码较多,为了不扰乱我们的视线,先将没有用到的代码屏蔽)
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) {
// no lock when adding to empty bin
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
......
}
}
addCount(1L, binCount);
return null;
}
由上述代码第二行可以看出,put方法中 key 与 value 均不可为 null 否则回报空指针异常。由第五行代码可知,每次添加新元素时,都会遍历整个map,直到找到可以放置新元素的位置为止。当调用put 第一次进入循环时,table 为null ,所以会调用 initTable 方法 来初始化Map,该方法详细内容见下方,简单来说就是Map的容量设置为默认大小16。第二次循环,table的长度经过初始化之后变成16,但里面每一个桶皆为null,所以会执行到 else if 中,并将元素放到计算的位置,当第一次添加到空桶(也可称空仓 empty bin ,即数组的该位置上没有元素)时不会加锁(源码中也给了很简明的注释)。
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;
}
当我们向map中添加足够多的元素时,hash碰撞是在所难免的,当产生hash碰撞时,在数组的同一个位置形成链表来装载不同元素,此时,为了防止多线程下出现数据出现问题,会在此条件下加锁,源码如下(暂时用不到的代码会屏蔽):
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;;) {
......
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;
}
上述代码表示,ConcurrentHashMap 的key 和value 均不可为null,否则直接抛出空指针异常。一旦添加元素时发生hash碰撞,就会找到碰撞的位置的链表,并加锁添加新的元素。直接看最重要的部分也就是最下面的判断,每次进入到锁中都会循环整个数组,知道找到对应的位置,而每一个循环binCount 就是自增1代表元素的个数,一旦达到或者超过默认的树化阀值,就会执行 treeifyBin 方法,即树化函数。
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));
}
}
}
}
}
进入到 treeifyBin 函数中,在执行真正的树化操作之前还会做一次判断,如果当前元素的个数小于 最小树化容量(64),则尝试扩容操作。否则就会执行树化操作 setTabAt 。
三、get() 方法
get函数比较简单,因为只是获取map中的内容,而没有对map本身有任何的修改操作,所以只是简单的了解以下,源码如下:
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;
}
从上述put函数可知,ConcurrentHashMap 的 key 和 value 均不可为 null ,否则报空指针。那么在 get 函数中 是否允许 key 为 null 呢,从源码中并没有直接判断 key 是否为 null 的代码,但是我们可以从第三行代码看出端倪,要想获取 key 对应的 value ,首先时要获取 key 的 hash 值,那么 key.hashCode() 就是获取 key 的 hash 值,如果 key 为null,那么可想可知 一定会报空指针异常。获取 key 的 hash 值之后就是根据 这个 hash 值 找到对应位置的value 返回,如果没有找到,那么直接返回 null 。
四、remove() 函数
先来一波源码,可以更容易理解:
public V remove(Object key) {
return replaceNode(key, null, null);
}
当我们执行remove操作的时候,底层实际是调用的 replaceNode ,根据这个函数名称,我们应该也能大概知道 他是怎么删除map中的元素的,先根据函数名猜测replaceNode 实际是根据key 找到对应的位置,并用 null 替换 该位置上的 value。接下来看下源码 是否跟我们猜测的一样:
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;
}
由上述代码看到 首先就是获取 key 的 hash 值,说明 在remove中的key也是不能为null的,删除的细节就不分析了,整体流程和putVal 相差不大,整体思路就是根据key找到对应位置的node 并将 此node 的value设置为null,执行替换操作之后 还会判断此节点是否为树节点的实例,也就是说 当前key所在的结构是否在树结构,再根据相应的条件 进行反树化操作。
本次先读到此处,后续有时间会加上。