大家都知道HashMap是线程不安全的,在高并发的情况下可能会发生键值对丢失,迭代失败等等的问题,于是为了在高并发环境下使用HashMap,ConcurrentHashMap应运而生,看名字(并发的HashMap)就可以知道该容器适合在并发环境下使用。ConcurrentHashMap是在java并发包(java.util.concurrent)下的一个类,在并发包中有很多的并发容器以及创建线程池的工具类(实现了ExecutorServie接口),而ConcurrentHashMap也是属于并发容器的一种,下面我们先来看看这个类的结构图:
由于Concurrent类的结构太过于复杂,这里只是选取了静态内部类,方法,静态常量各一部分方法,可以看到整个ConcurrentHashMap的设计和是实现是很负责且庞大的。
synchronized (f) {
if (tabAt(tab, 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)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node pred = e;
if ((e = e.next) == null) {
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;
}
}
}
}
其实无论是jdk1.7还是jdk1.8中,都是利用了分段锁的思想来实现的,只不过实现的方式不同罢了,有兴趣的同学可以自己去看一下jdk1.7的源码。那么又为什么说是分段锁的思想呢?这是因为在put方法中,synchronized只是对操作涉及到的桶加锁了,并没有对整张哈希表进行加锁,这就是分段锁的具体实现了,我们看代码中synchronized加锁的对象f,这个f对象在前面被赋值为(f = tabAt(tab, i = (n - 1) & hash)看到这这行代码是不是很熟悉呢,没错,就是根据hash值来计算元素在数组中的下标,注意这里的hash值并不同于hashcode值,关于此处的具体实现会在在下面详细分析,下面我们先来分析这个容器的一些新增的属性(相较于HashMap而言)
/**
* Minimum number of rebinnings per transfer step. Ranges are
* subdivided to allow multiple resizer threads. This value
* serves as a lower bound to avoid resizers encountering
* excessive memory contention. The value should be at least
* DEFAULT_CAPACITY.
*/
private static final int MIN_TRANSFER_STRIDE = 16; //最小转移步长
/**
* The maximum number of threads that can help resize.
* Must fit in 32 - RESIZE_STAMP_BITS bits.
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; //进行扩容所允许的最大线程数
/**
* The default concurrency level for this table. Unused but
* defined for compatibility with previous versions of this class.
*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16; //默认的并发等级,最多允许16个线程同时访问
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException(); //ConcurrentHashMap中不允许key为null或者value为null
int hash = spread(key.hashCode()); //计算key的hash值,计算方法与hashmap中不要太一样,加了一步操作,具体操作如下:return (h ^ (h >>> 16)) & HASH_BITS;
int binCount = 0;
for (Node[] tab = table;;) { //进入无限循环,将表的引用赋给tab
Node f; int n, i, fh;
if (tab == null || (n = tab.length) == 0) //如果表为空则初始化表,initTable()方法下面分析
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //否则计算key的数组下标并把该下标的引用赋值给f,如果为空则利用cas操作插入结点
if (casTabAt(tab, i, null,
new Node(hash, key, value, null)))
break; // no lock when adding to empty bin //当插入空bin中时不需要加锁,因为cas插入操作是原子性的
}
else if ((fh = f.hash) == MOVED) //如果f的hash为MOVED = -1,表明当前表正在扩容,给fh复制为f.hash
tab = helpTransfer(tab, f); //让该线程进入协助扩容
else { //否则对数组对应下标下的链表进行加锁
V oldVal = null;
synchronized (f) { //分段锁思想的代码实现,此处f已被复制并且不为null
if (tabAt(tab, i) == f) { //判断f的引用是否发生了改变,即是否被其他线程篡改过了
if (fh >= 0) { //如果fh>= 0表示当前表没有扩容
binCount = 1; //binCount为1表示该链表没有被树化
for (Node e = f;; ++binCount) { //无限循环遍历链表
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val; //如果key已经存在则保存旧值为oldVal
if (!onlyIfAbsent) //如果onlyIfAbsent为true,表示不允许覆盖,默认为false,表示允许覆盖旧值
e.val = value; //将value覆盖旧的value
break;
}
Node pred = e;
if ((e = e.next) == null) { //如果已经遍历至最后一个结点则创建一个新的结点插入,注意是尾插法
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) { //当前表没有进行扩容操作并且该数组下标所在的结点是一颗树结点,则进行红黑树的插入操作
Node p;
binCount = 2; //binCount为2表示该链表已经被树化
if ((p = ((TreeBin)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent) //此处操作的原因同上
p.val = value;
}
}
}
}
if (binCount != 0) { //binCount不为0表示链表或者红黑树发生了变化,需要进一步处理
if (binCount >= TREEIFY_THRESHOLD) //如果插入后链表结点数大于默认值8则进行链表的树形化
treeifyBin(tab, i);
if (oldVal != null) //oldVal为null说明存在相同的key,并且旧值已经被新值覆盖,返回旧值
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
put()方法总结:
1.判断key或value是否为空,为空则抛出异常
2.计算key的hash值并进入无限循环
3.如果表为空则初始化哈希表
4.如果表已存在,计算key对应的数组下标,并判断该数组下标所在的结点是否为空,为空则利用cas操作插入新界点,注意此处仍然不需要加锁,因为cas操作保证了这一步是原子性的
5.判断MOVED == -1,为真说明当前表正在扩容,调用helptransfer()方法协助扩容
6.否则锁定该条链表进入无限循环遍历链表,这里是分段锁的具体实现
7.接下来的步骤和HashMap中的步骤类似,这里不再赘述,需要注意的一点是onlyIfAbsent参数在这里得到了使用,代码中的注释已经写的很清楚了,还有一点就是ConcurrentHashMap也是使用了尾插法插入结点,因为头插法可能会导致意外情况发生,例如jdk1.7HashMap在并发环境下的死循环
8.插入结束
关于put()方法涉及到的其他方法暂时先不解释,以后会更新。
本文仅代表个人理解,如有不当,欢迎指正!