下面我们从HashTable开始,深入源码,分析HashTable、HashMap、ConcurrentHashMap这三个容器的区别。
public class Hashtable<K,V>zz
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
可以看到Hashtable继承了Dictionary字典,用于存储
boolean containsKey(Object key);
V get(Object key);
V put(K key, V value);
V remove(Object key);
int size();
boolean isEmpty();
更多的抽象方法请查看API-Map
private transient Entry,?>[] table;
private transient int count;
private int threshold;
private float loadFactor;
private transient int modCount = 0;
我们可以看到变量中有个修饰符:transient,使用transient修饰的变量当对象存储时,它的值不需要维持。通俗一点说,transient关键字标记的变量不参与序列号过程
Entry
/**
* Hashtable bucket collision list entry
*/
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry next;
protected Entry(int hash, K key, V value, Entry next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,(next==null ? null : (Entry) next.clone()));
}
/*
省略get/set方法
*/
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry,?> e = (Map.Entry,?>)o;
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
public int hashCode() {
return hash ^ Objects.hashCode(value);
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
hash:存储当前对象的hash散列值
key、value:用于存储对象的key和value值,并且key声明为final,表示对象的key赋值后不可修改
Entry next:说明Entry是一个链表,next用来连接下一个Entry所在的地址
count 存储当前HashTable的Entry对象的数量
关于 threshold、loadFactor以及capacity这三个值,threshold、loadFactor是两个成员变量值,而capacity是HashTable对象初始化时,构造函数的一个输入参数。
threshold,表示当前HashTable能够存储的最大数量,如果当前对象数量>=threshold,则Hashtable就会ReHash,对Hashtable进行扩容。
Rehash的具体内容请见后文。
threshold = capacity * loadFactor。loadFactor又叫负载因子,指当前HashTable的使用率,默认的值为0.75,这个值不能太大也不能太小,如果太大,则HashTable过满,某一个散列值槽位的链表可能会过长,导致查询的效率降低;如果太小,则HashTable的利用率就会变低,浪费了空间资源。
capacity是值Hashtable的大小,在创建HashTable时,如果调用下面的构造函数,则输入一个HashTable的初始大小。
public Hashtable(int initialCapacity, float loadFactor);
public Hashtable(int initialCapacity);
如果调用无参数的构造方法
public Hashtable() {
this(11, 0.75f);
}
capacity的值默认为11
Hashtable(int initialCapacity)和Hashtable() 都会通过this(initialCapacity, loadFactor)调用下面的构造方法
默认loadFactor为0.75
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
如果initialCapacity小于0则返回不合法初始异常,如果initialCapacity等于0,则初始initialCapacity为1
其他情况则初始化Entry数组,且赋值threshold
这里有一个MAX_ARRAY_SIZE的定义如下:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
modCount = 0 值当前HashTable 修改的次数(指HashTable长度的变化修改),用来实现“fail-fast”机制的(也就是快速失败)。所谓快速失败就是在并发集合中,其进行迭代操作时,若有其他线程对其进行结构性的修改,这时迭代器会立马感知到,并且立即抛出ConcurrentModificationException异常,而不是等到迭代完成之后才告诉你(你已经出错了)。
在HashTable中,基本所有的方法都由synchronized关键字修饰,来解决并发问题,这也是HashTable性能低下的根本原因。至于synchronized关键字的原理,请见csdn
public synchronized int size() {
return count;
}
实现的是接口Map
public synchronized boolean isEmpty() {
return count == 0;
}
实现的是接口Map
public synchronized V get(Object key) {
Entry,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
get方式也是实现的Map中的get方法,通过key值,查询到key值对于的value,如果当前对象不存在,就返回null
方法中,首先将当前的散列表赋值给新的tab,然后获取当前key的hash值,然后对hash值对tab数组的长度进行取余运算,得到当前所在的数组下标,并对当前槽位的链表进行遍历,找到hash值相同且key值相等
这里需要注意:hash值相同但key值不一定相等
hash & 0x7FFFFFFF 保证了得到的索引值是一个正数
这里通过先比较hash值,来提高查找的效率(hash值的获取比对象的获取更加的高效)
put方法
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
Entry entry = (Entry)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
通过实现Map
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry,?> tab[] = table;
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
Entry e = (Entry) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
首先将modCount的值加1,这里判断了一下table是否需要扩容,如果需要扩容就进行rehash()扩容,并重新获取相应的table、hash以及index数组下标
最后将通过给tab[index]赋予新的值,将新的Entry对象添加到链表头,并将count++
可以发现,HashTable存储的key和value都是不能为空的
HashTable的rehash
这里讲到了put方法中,如果当前Table的对象数量达到了threshold,就会进行rehash。rehash的原理也比较简单,就是新建一个table且容量扩大两倍+1,并将数组中对应的链表迁移到新的table中
protected void rehash() {
int oldCapacity = table.length;
Entry,?>[] oldMap = table;
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry,?>[] newMap = new Entry,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry old = (Entry)oldMap[i] ; old != null ; ) {
Entry e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry)newMap[index];
newMap[index] = e;
}
}
}
可以看到扩容是将初始容量进行移位操作,扩大两倍+1,创建一个新的Entry数组,将threshold重新初始化,并将新的Map数组赋值给table,最后遍历所有的Entry对象,迁移到新的table上。结果如图所示:
这里需要注意的一个地方是:
扩容后的链表和原链表的顺序是相反的
remove
public synchronized V remove(Object key) {
Entry,?> tab[] = table;
int hash = key.hashCode();
Entry e = (Entry)tab[index];
for(Entry prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
这里的remove操作就是找到key对于的Entry对象并将此对象的前一个Entry指向后一个Entry,并返回删除的Entry对象
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
可以发现在接口实现上,相比HashTable是相同的,只是在继承上,HashMap继承的是AbstractMap,查看API文档可以知道,AbstractMap是一个抽象类,实现了Map
static class Node implements Map.Entry
Entry的实现上来看,最大的区别就在于hashCode的函数实现
HashTable中:
public int hashCode() {
return hash ^ Objects.hashCode(value);
}
HashMap中:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
由此可以看出,HashMap中,key和value的值可以是null。并且当key为空时,获取到的hash值为0,表示当前对象放置在第一个槽位。HashMap也引入了高位运算>>>逻辑右移,,将低十六位和高十六位进行异或,尽量减少hash冲突
讲到Hash冲突,这里着重讲一下HashMap在Hash运算的两点高明的改进之处:
- 一、 计算hashcode时引入高16位与低十六进行运算,在我们利用hashcode计算index时,我们知道是用(n-1)& hash 来得到下标,我们知道HashMap中 table的长度都是2的n次幂(详情请见下面的构造函数差别),因此index仅与hash值的低n位相关,假如table的length为 16=24 16 = 2 4 ,则index仅与低4位有关,很容易出现hash冲突。引入高位能够尽量的避免这样的情况出现
- 二、 在计算threshold时,利用下面这个函数。
我们在HashTable中了解到,threshold是initialCapacity*loadFactory。threshold是一个阈值,判断当前是否应该扩容。而在HashMap中,利用tableSizeFor函数,来进行threshold的赋值,构造函数初始化时没有用到loadFactory,这是因为构造函数没有初始化table,当调用put方法时才初始化table,相当于延迟初始化的方法(后面讲resize扩容时会讲到)。
下面我先讲一下这个算法的原理。
我们知道,table的初始长度capacity肯定是2的幂,因此,这个算法主要用于找到大于等于initialCapacity的最小的2的幂,如果等于则就返回这个数。首先我们看到将 int = cap-1,这是为了防止当cap已经是2的幂次方时,没有执行减1操作,则执行完这个算法时,得到的将会是一个cap的两倍。
如果n为0,则经过几次无符号右移依然是0,最后返回的是capacity是1(最后有一个n+1操作)
下面讲的是n不为0的情况
第一次右移: n |= n >>> 1;
因为在n不为0,始终有一位为1。首先进行向右移位,则最高为1的位置向右移动1位;然后将n与移位后进行或运算,则得到的结果为,最高为1的位置和其右边的一位也为1。则就有连续的两个1
第二次右移: n |= n >>> 2;
进过第一次运算后,最高位两个为1的位置是连续的,通过右移两位,再进行或运算,则得到最高连续4位为1
第三次右移: n |= n >>> 4;
第三次右移后,得到最高位后连续八位为1,后面的以此计算。
因为容量最大为32位,当32位全为1后,最大容量为:MAXIMUM_CAPACITY。
提醒两点:
1、这有一个规律,任何不为2幂次方的数,他的最高位是1的位置始终是大于等于它为2的幂次方的数的最高位为1的后面一位。可能这句话有点绕口,我举个例子:假如为3,它的二进制位0011,比它大且为2的幂次的数为4,二进制为0100,刚好比3的最高为1的位置多了一位。
2、尽管每次运算都要执行到右移16位,但是当一个数的最高为1的位置不足16位或者8位或者4位时,后面高的移位操作且求或运算后,这个数依旧不变
3、最后得到的数就是将它最开始的最高为1的位右边的所有位全部变为1,最后执行一个+1操作就可以得到大于等于它的最小为2的幂次方的数。不得不说此算法的高明之处!
//静态函数
static final int tableSizeFor(int cap) {
int n = cap - 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;
}
//构造函数中调用:
this.threshold = tableSizeFor(initialCapacity);
举一个例子:
HashMap中的主要成员变量和HashTable相同:threshold,loadFactor,size,modCount。前面已经说过了这些变量的含义,这里就不再赘述。
构造函数
同HashTable,HashMap的构造函数也有4个,具体请参照上文HashTable,这里说明一下其中的细小区别,其他不再赘述。
this.threshold = tableSizeFor(initialCapacity);
这里将tableSizeFor得到大于等于initialCapacity的最小2的幂的整数赋值给threshold,而不是通过capacity*loadFactory。在讲算法的时候已经讲过,这里并没有初始化table,而是在调用put方法时,判断为空,在resize中进行初始化。后面讲put方法和扩容时会讲到
get函数
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
可以看到这里调用的是getNode函数进行查找
再看getNode()函数源码:
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
/*
*判断当前table是否是空,如果为空直接返回null
*当tab不为空,tab.length>0,且tab对应槽位的链表也不为空时,进行查找
*/
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//判断第一个Node是否是要查找的对象
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果链表头结点的next不为空则继续遍历查找
if ((e = first.next) != null) {
//如果头结点为一个树的根节点,则进行树的遍历
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
//如果不为树的根节点,则遍历链表
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
这个函数有一点需要注意:
当头结点不为空且头结点的next也不为空时,这里有一个分支判断:如果头结点是树节点,则进行树的遍历,如果不是,则进行链表的遍历。在JDK 1.7以前,槽位全部使用的是链表,而JDK1.8以后,加入了红黑树(对于红黑树的原理,后面会有相关的文章,此时请自己查找相关资料),此时只需要知道红黑树是一种特殊的自平衡二叉树就行。对于查找来说,红黑树的效率会高很多。毕竟使用过二叉树的都应该知道,二分法查找。在JDK 1.8以后,当某一个槽位的节点数量大于8时,使用红黑树。当小于或等于6时,使用链表。此时有一个神奇的数量:7,当进行put和remove使数量在7左右变换的时候,效率将会非常低,其中的问题请自主思考!这里就不展开说明红黑树了。
put函数
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
这里可以看到在put中调用了putVal进行插入操作,下面我们看putVal的源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
//首先判断tab是否没有初始化:为空
if ((tab = table) == null || (n = tab.length) == 0)
//如果没有初始化,则调用resize进行初始化,并返回初始化后tab的length
n = (tab = resize()).length;
//同样根据hash值找到对应index的槽位是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
//如果为空,则直接新建一个Node并赋值给对应的槽位
tab[i] = newNode(hash, key, value, null);
else {
//如果不为空
Node e; K k;
//判断第一个头结点是否为要插入的Node对象
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果头结点和要插入的对象不同,判断头结点是否是树节点
else if (p instanceof TreeNode)
//如果是树节点,则将对应的节点
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
//如果头结点不是树节点,则将对应的Node节点插入链表中
//遍历链表
for (int binCount = 0; ; ++binCount) {
//如果next为空,则直接将新的Node插入到尾结点后
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//判断当前节点是否大于八个(新插入了一个节点),如果是则将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果新节点存在,则直接退出查找并返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e不为空,则表示要插入的节点已经存在
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//插入后将size+1 并且判断当前的size是否大于threshold阈值,大于就进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
这里有一点需要说明:
afterNodeAccess(e)、afterNodeInsertion(evict)在HashMap中的函数体为空,是一个用于LinkedHashMap的回调函数,在HashMap中没有用处。在LinkedHashMap中,将这些函数进行实现。
resize
前面在put和get方法中都用到了resize,就是HashMap的扩容操作。下面将会具体讲述HashMap中resize的原理和其与HashTable中的不同之处
在分析resize源码之前,我们根据前面所了解的知识,可以粗略总结一下resize需要完成的工作有以下几点:
- 当put Node对象节点时,会判断当前的table是否为空,如果为空就会进行resize()完成table的初始化操作
- 当插入一个节点后,如果当前HashMap的Node数量count>threshold,就会通过resize()进行扩容
- 对table进行扩容后,将原来对于槽位的数据迁移到新的table数组上
- 重新计算threshold
下面来看resize的源码
final Node[] resize() {
//将需要扩容的table,赋值给新的table上
Node[] oldTab = table;
//判断当前的table是否为空,如果为空,则当前的数组长度设置为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//将当前的阈值赋值给oldThr
int oldThr = threshold;
//初始化新的数组长度和threshold
int newCap, newThr = 0;
//如果table不为空,则table长度>0
if (oldCap > 0) {
//判断当前的table数组长度是否超过了最大(默认为2的30次方)
if (oldCap >= MAXIMUM_CAPACITY) {
//如果超过就将阈值设置为int所能表示的最大长度2的31次方,并返回oldTab。不扩容,只修改阈值
threshold = Integer.MAX_VALUE;
return oldTab;
}//将table的长度扩大为2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 如果table长度大于默认的初始长度16,设置阈值为原来的两倍
}//当table为空,且阈值大于0
else if (oldThr > 0) // 新的初始化table长度设置为阈值大小
newCap = oldThr;
else { //当table为空且阈值也小于0,即调用了无参数的构造函数。(仅设置了负载因子为默认的0.75)
newCap = DEFAULT_INITIAL_CAPACITY; //table的容量设置为默认的16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //新的阈值为16*0.75
}
/*进入下面这个if语句的条件是:
*当创建HashMap的时候输入了一个小于16的初始大小,前面虽然将长度扩大两倍,但是不满足大于16的要求
*或者是第一次扩容初始化Table,且构造函数传入了一个初始容量
*此时newThr为0
*/
if (newThr == 0) {
//将newThr设置为初始化长度*负载因子
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将newThr赋值给阈值,并用新的长度(扩大两倍后),创建新的newtable,并将新的newtable赋值给table
threshold = newThr;
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
/*
* 后面就是通过遍历将原来的节点通过hash迁移到新的table上,返回新的table。
* 省略迁移的代码,详情请查看hashTable的迁移,
* 这里多一点就是只是需要判断一下是否为红黑树头结点
*/
return newTab;
}
resize需要注意两点:
要分清楚扩容分三种情况:
- table不为空:分两种情况:table的长度小于16和table的长度大于16(HashMap不是第一次执行resize)
- table为空,但是阈值不为空(HashTable调用单参数或双参数构造函数创建,第一次执行resize)
- table为空,且阈值也为空(HashTable调用无参数构造函数创建,第一次执行resize)
在这里HashMap最主要的几个函数就已经分析完了,如有需要请查看JDK源码。
分析一下HashTable和HashMap的区别:
- 1、HashTable是线程安全的而HashMap不是线程安全的,这一点显而易见
- 2、HashTable的key和value不能为空,而HashMap的key和value可以为空。通过hash运算可以看出,HashMap中key为null的Node,hash值为0,则对于的节点都是存储在槽位为0(即第一个槽位上)
- 3、HashTable中的节点以链表形式存储,而HashMap中,jdk1.8及以后既有链表也有红黑树
- 4、HashTable的table长度不是2的幂次,而HashMap的table长度是2的幂次,所有HashMap的效率会更高。下标运算时使用或运算比取余运算更加的高效.
- 5、HashMap在求key的hashcode时,引入了高16位,减少了hash冲突
虽然HashMap从很多地方看,在HashTable的基础上做了很大的修改和提升,但依旧有很大的问题。如果是单线程的情况下,使用HashMap的效率很高,但是在多线程并发的情况下,HashMap就会出现问题。有一种解决方案就是给HashMap对象加锁,但是这样虽然防止了对象共享的问题,但整个对象加锁的效率就会非常低,即使是多读少写的情况,无论是读或者写,当一个线程拿到当前HashMap的锁后,其他的线程都不能进行操作只能等待当前线程操作结束释放锁。在这个基础上,ConcurrentHashMap的出现就给并发情况下的HashMap提供了解决方案。
注意:因为jdk 1.8之前的ConcurrentHashMap使用的Segment分段锁技术,而1.8后就舍弃了分段锁技术,采用了CAS乐观锁机制,只有在节点头访问时例如table[i]才会加锁,对其中的变量采用cas院子操作
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable
我们可以看到,此处ConcurrentHashMap 继承了AbstractMap,和HashMap相同。但是这里ConcurrentHashMap实现了ConcurrentMap,与HashMap不同。我们通过ConcurrentMap可以看到
public interface ConcurrentMap<K, V> extends Map<K, V>
ConcurrentMap继承了接口Map,由此可见,此处和HashMap中实现Map接口是一样的。不过,在代码上将,ConcurrentMap的方法和AbstractMap的方式虽然都来自Map,但继承和实现却不同。
此处,我们可能会产生一个疑惑,为什么ConcurrentMap接口继承了Map接口,在Thinking in java中也没有讲到接口继承的问题,但我们都知道Java中,接口可以多实现,只能单继承。而接口不能实现另一个接口,所以一个接口可以继承其他的接口。
常量
下面说几个在JDK1.8中新增的常量
/*
* 最大的扩容线程的数量,如果上面的 RESIZE_STAMP_BITS = 32,那么此值为 0,这一点也很奇怪。
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
/*
* 移位量,把生成戳移位后保存在sizeCtl中当做扩容线程计数的基数,相反方向移位后能够反解出生成戳
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
/* ForwardingNode的hash值,ForwardingNode是一种临时节点,在扩进行中才会出现,并且它不存储实际的数据
* 如果旧数组的一个hash桶中全部的节点都迁移到新数组中,旧数组就在这个hash桶中放置一个ForwardingNode
* 读操作或者迭代读时碰到ForwardingNode时,将操作转发到扩容后的新的table数组上去执行,写操作碰见它 * 时,则尝试帮助扩容 */
static final int MOVED = -1; // hash for forwarding nodes
/* TreeBin的hash值,TreeBin是ConcurrentHashMap中用于代理操作TreeNode的特殊节点,持有存储实际数据的红黑树的根节
* 点因为红黑树进行写入操作,整个树的结构可能会有很大的变化,这个对读线程有很大的影响,所以TreeBin 还要维护一个简单读写锁,这是相对HashMap,这个类新引入这种特殊节点的重要原因
*/
static final int TREEBIN = -2; // hash for roots of trees
/* ReservationNode的hash值,ReservationNode是一个保留节点,就是个占位符,不会保存实际的数据,正常情况是不会出现的,在jdk1.8新的函数式有关的两个方法computeIfAbsent和compute中才会出现*/
static final int RESERVED = -3; // hash for transient reservations
/* 用于和负数hash值进行 &运算,将其转化为正数(绝对值不相等),Hashtable中定位hash桶也有使用这种方式来进行负数转正数*/
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
成员变量
//当前的table数组
transient volatile Node[] table;
//扩容后的新的table数组,只有在扩容时不为空
//通过判断newTable !=null,一般可以认为当前还有线程在进行扩容
private transient volatile Node[] nextTable;
/**
*下面三个主要与统计数目有关,
*可以参考jdk1.8新引入的java.util.concurrent.atomic.LongAdder的源码,帮助理解
*计数器基本值,主要在没有碰到多线程竞争时使用,需要通过CAS进行更新
*/
private transient volatile long baseCount;
/**
* 非常重要的一个属性,具体的含义通过后面扩容进行解读。
* 其实也就是一个扩容的阈值以及用于多线程的扩容互斥(即当一个线程扩容时,其他线程不能扩容)
*/
private transient volatile int sizeCtl;
/**
* 保证一个transfer任务不会被几个线程同时获取(相当于任务队列的size减1)
*/
private transient volatile int transferIndex;
/**
*CAS自旋锁标志位,用于初始化,或者counterCells扩容时
*/
private transient volatile int cellsBusy;
/**
* 用于高并发的计数单元,如果初始化了这些计数单元,那么跟table数组一样,长度必须是2^n的形式
*/
private transient volatile CounterCell[] counterCells;
可以看到所有的变量都是通过volatile关键字进行了修饰,对于volatile关键的原理请查询相关资料,这里简单说一下,通过volatile关键字修饰的变量,保证了内存可见性以及禁止指令重排,不能保证原子性(即volatile单独使用不能解决并发问题)
在ConcurrentHashMap中,Node也是继承了Map.Entry
static final class TreeBin extends Node {
TreeNode root;
volatile TreeNode first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
/**
* Creates bin with initial set of nodes headed by b.
*/
TreeBin(TreeNode b) {
super(TREEBIN, null, null, null);
this.first = b;
TreeNode r = null;
for (TreeNode x = b, next; x != null; x = next) {
next = (TreeNode)x.next;
x.left = x.right = null;
if (r == null) {
x.parent = null;
x.red = false;
r = x;
}
else {
K k = x.key;
int h = x.hash;
Class> kc = null;
for (TreeNode p = r;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
r = balanceInsertion(r, x);
break;
}
}
}
}
this.root = r;
assert checkInvariants(root);
}
//...................省略其他的方法
在ConcurrentHashMap中,还有一个辅助内部类,ForwardingNode。他继承于Node节点,是一个临时节点,在扩容进行中才会出现,hash值固定为-1,并且它不存储实际的数据。在扩容的时候,旧数组的一个槽位中的节点全部迁移到新数组中,旧数组就在这个槽位上放置一个ForwardingNode。当读操作或者迭代操作碰到ForwardingNode时,将作转发到扩容后的新的table数组上去执行,写操作碰到时,将会直接在新的tab上写。
static final class ForwardingNode extends Node {
final Node[] nextTable;
ForwardingNode(Node[] tab) {
super(MOVED, null, null, null); // 此节点hash=-1,key、value、next均为null
this.nextTable = tab;
}
Node find(int h, Object k) {
// 查nextTable节点,outer避免深度递归
outer: for (Node[] tab = nextTable;;) {
Node e; intn;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
returnnull;
for (;;) { // CAS算法多和死循环搭配!直到查到或null
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
returne;
if (eh < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode)e).nextTable;
continue outer;
}
else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
}
注意:在forwarding中,值是一个标志节点,指向newTable,并提供一个find方法,帮助找到在新的table中index对应的节点。
构造函数
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;
}
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;
}
构造函数主要做了两件事(和HashMap差不多):
- 1、参数的有效性检查
- 2、table初始化的长度(如果不指定默认情况下为16)。
这里要说一个参数:concurrencyLevel,表示能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数。默认值为16,(即允许16个线程并发可能不会产生竞争)。仅仅是为了兼容旧版本而保留
三个原子操作
static final Node tabAt(Node[] tab, int i) { // 获取索引i处Node
return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
// 利用CAS算法设置i位置上的Node节点(将c和table[i]比较,相同则插入v)。
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);
}
这三个原子操作调用频率很高。而且原子操作的效率和并发性都得到了保证
先讲最简单的get方法
get方法很简单,对于key的hash和value查到相应的值,节点可能对于链表或者红黑树
public V get(Object key) {
Node[] tab; Node e, p; int n, eh; K ek;
//计算hash值
int h = spread(key.hashCode());
//根据hash值确定节点位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//如果搜索到的节点key与传入的key相同且不为null,直接返回这个节点
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//如果eh<0 说明这个节点在树上 直接寻找
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;
}
在这里有一个spread二次hash运算,和HashMap中的hash函数相同,也是将高16位引入计算并且保证为正数
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
static final int HASH_BITS = 0x7fffffff;
put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
在put方法中调用putVal,下面我们分析putVal的源码
final V putVal(K key, V value, boolean onlyIfAbsent) {
//判断key和value是否为空,如果为空就抛出异常
if (key == null || value == null) throw new NullPointerException();
//通过二次hash运算得到key的hashcode
int hash = spread(key.hashCode());
//用于记录相应链表的长度
int binCount = 0;
//开始死循环,直到插入成功
for (Node[] tab = table;;) {
Node f; int n, i, fh;
//如果数组为空,则进行初始化。对应的函数在后面会分析
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//否则,通过原子操作tabAt找到index对应的头节点
//如果为空就通过CAS原子操作直接插入到对应的槽位,如果失败,则表示有并发操作,退出进入下一个循环
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//如果对应的位置不为空,判断是否等于MOVED,如果等于MOVED,表示当前槽位正在进行扩容
else if ((fh = f.hash) == MOVED)
//帮助数据迁移,等后面分析对应的源码就很容易理解
tab = helpTransfer(tab, f);
//如果没有进行扩容
else {
V oldVal = null;
//对数组该位置的头结点进行加锁
synchronized (f) {
//判断在查找的过程中是否有改变,通过再次获取头结点以及判断是否相等
if (tabAt(tab, i) == f) {
if (fh >= 0) { 如果hash值大于0表示是链表
binCount = 1;
for (Node e = f;; ++binCount) {
K ek;
//如果找到相等的key,判断是否需要覆盖
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
//否则将Node假如到链表末尾
Node pred = e;
if ((e = e.next) == null) {
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
//如果是树节点,将Node添加到红黑树中
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)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
put方法看完,留下三个问题,第一个是初始化table,第二个是扩容,第三个问题是帮助数据迁移(helpTrasfer)。下面将会进行一一介绍
初始化:initTable
private final Node[] initTable() {
Node[] tab; int sc;
//判断当前的table是否为空,如果为空则进行初始化
while ((tab = table) == null || tab.length == 0) {
//这里使用sizeCtl判断是否其他线程正在进行初始化
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//如果没有其他线程正在进行初始化,则通过CAS将sizeCtl设置为-1,表示抢到了锁
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
//DEFAULT_CAPACITY 初始容量为16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
Node[] nt = (Node[])new Node,?>[n];
table = tab = nt;
//这里就是ConcurrentHashMap为什么不适用LoadFactory的原因了
//如果n=16,则sc = 12,相当于0.75*n
sc = n - (n >>> 2);
}
} finally {
//最后将sizeCtl设置为sc大小,扩容结束,并且
sizeCtl = sc;
}
break;
}
}
return tab;
}
treeifyBin
这个函数刚会在put方法中也见到过,就是将链表转换为红黑树,但是treeifyBin不一定会进行转换,也可能仅仅是对数组进行扩容。下面我们将会具体讲到相关的源码。
private final void treeifyBin(Node[] tab, int index) {
Node b; int n, sc;
if (tab != null) {
//首先判断数组长度是否小于64,如果小于64(其实就是16,32等)就会进行数组扩容
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
//扩容函数,后面讲到
tryPresize(n << 1);
//不扩容,找到对应index的头结点
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) { //加锁,进行转换
if (tabAt(tab, index) == b) {
TreeNode hd = null, tl = null;
//下面就是遍历链表,转换为红黑树
for (Node e = b; e != null; e = e.next) {
TreeNode p =
new TreeNode(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(hd));
}
}
}
}
}
在这里大家可能会有一个疑问:为什么数组长度小于64,就只进行扩容而不转换为红黑树呢。首先,MIN_TREEIFY_CAPACITY是一个系统定义的值,当数组长度小于64时,对数组进行扩容,能够明显的解决hash冲突从而减小链表长度以此来避免对链表转化为红黑树
扩容
private final void tryPresize(int size) {
//如果size大于等于默认最长数组长度的1/2,则使用默认最长的长度。否则将size*1.5+1,向上取最近的2的n次方(其实这里也有一个疑问,在整个ConcurrentHashMap中,table的长度都是2的幂次,为什么这里不直接使用size。而且在调用扩容时,先讲table.len<<1,这里再将size*1.5去最近的2的幂次不是相当于4倍了么)
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
//执行扩容,后面的和初始化相差不大,最主要的就是调用transfer进行迁移
while ((sc = sizeCtl) >= 0) {
Node[] tab = table; int n;
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
Node[] nt = (Node[])new Node,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) {
int rs = resizeStamp(n);
if (sc < 0) {
Node[] nt;
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进行迁移,后面会讲到
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
transfer
下面这个方法很点长,将原来的 tab 数组的元素迁移到新的 nextTab 数组中。
虽然我们之前说的 tryPresize 方法中多次调用 transfer 不涉及多线程,但是这个 transfer 方法可以在其他地方被调用,典型地,我们之前在说 put 方法的时候就说过了,请往上看 put 方法,是不是有个地方调用了 helpTransfer 方法,helpTransfer 方法会调用 transfer 方法的。
此方法支持多线程执行,外围调用此方法的时候,会保证第一个发起数据迁移的线程,nextTab 参数为 null,之后再调用此方法的时候,nextTab 不会为 null。
阅读源码之前,先要理解并发操作的机制。原数组长度为 n,所以我们有 n 个迁移任务,让每个线程每次负责一个小任务是最简单的,每做完一个任务再检测是否有其他没做完的任务,帮助迁移就可以了,而 这里使用了一个 stride,简单理解就是步长,每个线程每次负责迁移其中的一部分,如每次迁移 16 个小任务。所以,我们就需要一个全局的调度者来安排哪个线程执行哪几个任务,这个就是属性 transferIndex 的作用。
第一个发起数据迁移的线程会将 transferIndex 指向原数组最后的位置,然后从后往前的 stride 个任务属于第一个线程,然后将 transferIndex 指向新的位置,再往前的 stride 个任务属于第二个线程,依此类推
private final void transfer(Node[] tab, Node[] nextTab) {
int n = tab.length, stride;
// stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16
// stride 可以理解为”步长“,有 n 个位置是需要进行迁移的,
// 将这 n 个任务分为多个任务包,每个任务包有 stride 个任务
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 如果 nextTab 为 null,先进行一次初始化
// 前面我们说了,外围会保证第一个发起迁移的线程调用此方法时,参数 nextTab 为 null
// 之后参与迁移的线程调用此方法时,nextTab 不会为 null
if (nextTab == null) {
try {
// 容量翻倍
Node[] nt = (Node[])new Node,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
// nextTable 是 ConcurrentHashMap 中的属性
nextTable = nextTab;
// transferIndex 也是 ConcurrentHashMap 的属性,用于控制迁移的位置
transferIndex = n;
}
int nextn = nextTab.length;
// ForwardingNode 翻译过来就是正在被迁移的 Node
// 这个构造方法会生成一个Node,key、value 和 next 都为 null,关键是 hash 为 MOVED
// 后面我们会看到,原数组中位置 i 处的节点完成迁移工作后,
// 就会将位置 i 处设置为这个 ForwardingNode,用来告诉其他线程该位置已经处理过了
// 所以它其实相当于是一个标志。
ForwardingNode fwd = new ForwardingNode(nextTab);
// advance 指的是做完了一个位置的迁移工作,可以准备做下一个位置的了
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
/*
* 下面这个 for 循环,最难理解的在前面,而要看懂它们,应该先看懂后面的,然后再倒回来看
*
*/
// i 是位置索引,bound 是边界,注意是从后往前
for (int i = 0, bound = 0;;) {
Node f; int fh;
// 下面这个 while 真的是不好理解
// advance 为 true 表示可以进行下一个位置的迁移了
// 简单理解结局:i 指向了 transferIndex,bound 指向了 transferIndex-stride
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
// 将 transferIndex 值赋给 nextIndex
// 这里 transferIndex 一旦小于等于 0,说明原数组的所有位置都有相应的线程去处理了
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
// 看括号中的代码,nextBound 是这次迁移任务的边界,注意,是从后往前
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
// 所有的迁移操作已经完成
nextTable = null;
// 将新的 nextTab 赋值给 table 属性,完成迁移
table = nextTab;
// 重新计算 sizeCtl:n 是原数组长度,所以 sizeCtl 得出的值将是新数组长度的 0.75 倍
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
// 然后,每有一个线程参与迁移就会将 sizeCtl 加 1,
// 这里使用 CAS 操作对 sizeCtl 进行减 1,代表做完了属于自己的任务
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 任务结束,方法退出
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
// 也就是说,所有的迁移任务都做完了,也就会进入到上面的 if(finishing){} 分支了
finishing = advance = true;
i = n; // recheck before commit
}
}
// 如果位置 i 处是空的,没有任何节点,那么放入刚刚初始化的 ForwardingNode ”空节点“
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 该位置处是一个 ForwardingNode,代表该位置已经迁移过了
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 对数组该位置处的结点加锁,开始处理数组该位置处的迁移工作
synchronized (f) {
if (tabAt(tab, i) == f) {
Node ln, hn;
// 头结点的 hash 大于 0,说明是链表的 Node 节点
if (fh >= 0) {
// 下面这一块和 Java7 中的 ConcurrentHashMap 迁移是差不多的,
// 需要将链表一分为二,
// 找到原链表中的 lastRun,然后 lastRun 及其之后的节点是一起进行迁移的
// lastRun 之前的节点需要进行克隆,然后分到两个链表中
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);
}
// 其中的一个链表放在新数组的位置 i
setTabAt(nextTab, i, ln);
// 另一个链表放在新数组的位置 i+n
setTabAt(nextTab, i + n, hn);
// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
// 其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
setTabAt(tab, i, fwd);
// advance 设置为 true,代表该位置已经迁移完毕
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;
}
}
// 如果一分为二后,节点数少于 8,那么将红黑树转换回链表
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin(hi) : t;
// 将 ln 放置在新数组的位置 i
setTabAt(nextTab, i, ln);
// 将 hn 放置在新数组的位置 i+n
setTabAt(nextTab, i + n, hn);
// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
// 其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
setTabAt(tab, i, fwd);
// advance 设置为 true,代表该位置已经迁移完毕
advance = true;
}
}
}
}
}
}
helptransfer
/**
* Helps transfer if a resize is in progress.
*/
final Node[] helpTransfer(Node[] tab, Node f) {
Node[] nextTab; int sc;
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
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;
}
helptransfer的原理很简单,就是判断当前节点是否已经搬移到新的table中,如果是,则表示当前table正在进行扩容,这个线程将会帮助扩容。如果没有搬迁,则在当前节点上进行操作
不得不说在ConcurrentHashMap中,需要理解的方法很多,这也是在高效提升后所带来的代码复杂性。尤其是在JDK 1.8后,对ConcurrentHashMap的改进后,理解起来更加的困难。但我们从源码上一句一句的理解过后,我们会发现很多东西。下面将会对ConcurrentHashMap做一个总结,包括与HashMap的比较以及在JDK 1.8之前ConcurrentHashMap与JDK 1.8有什么区别。
对于HashMap和ConcurrentHashMap的区别
- HashMap不是线程安全的,ConcurrentHashMap是线程安全的。
- HashMap的key和value可以为null,ConcurrentHashMap不能为空
- 当线程安全的使用HashMap时,ConcurrentHashMap效率会高出很多
对于JDK1.7和JDK1.8中ConcurrentHashMap的区别
- 最重要的是安全机制,jdk1.7中 采用的是Segment分段锁机制,Segment继承于ReentrantLock这种可重入锁。对每一段table进行加锁。而在Jdk1.8中,舍弃了分段锁机制,采用CAS+Synchronized机制实现了更小粒度的锁,即对table的每一个index进行加锁同样采用很多的原子操作,也更加的提升了效率。
- 在put方法,1.7通过两次hash运算,先后得到哪一段,哪一个index,获取到对应的链表或者红黑树,最后通过trylock()对整段进行加锁,其他线程挂起。1.8中,通过hash运算得到index,获取到头结点,此时有三种情况。如果头结点为空,则直接通过CAS操作将Node put进去。如果不为空但hash为move,则表示有其他线程正在进行扩容,参与一起扩容,头结点不为空且hash不为move,则对头结点进行synchronized加锁,遍历put。
- get 方法大致相同,因为变量都是通过volatile修饰,保证了内存可见性。当修改数据后,能够将缓存中的值立马写到主存中,保证并发下能够读到最新的数据。
- resize方法 1.7是对每个segment进行扩容,和HashMap相似,不过会进行lock,是单线程进行扩容。而在1.8中,则能够进行多线程扩容,让访问到对应的节点为Forwarding,则参与扩容。
- size()方法,在1.7中采用先不加锁获取两次,如果相同则正确,不一样则把所有的segment锁住,计算size。在1.8中,采用了一个baseCount变量来记录当前节点的个数,通过调用sumCount来计算baseCount和counterCells存储的修改次数的和,最后获取到size。