Java的集合框架分为两类:
- 以ArrayList,LinkedList,HashMap为主的非线程安全类框架
- 以Vector,ConcurrentHashMap,HashTable为主的线程安全类框架
他们分别适用于不同场景,前面已经对其中的部分做了正对性介绍这里就不做过多的描述了。
本文重点分析线程安全的框架,同时列举其应用场景。
ConcurrentHashMap
ConcurrentHashMap的结构和HashMap的结构相似,也是继承一个实现一个。和HashMap的区别在于HashMap继承的是Map接口而ConcurrentHashMap继承的是ConcurrentMap接口。
ConcurrentHashMap的基本代码结构,框架与HashMap类似,HashMap在前面已经做了比较全面的概述,这里我们将着重分析ConcurrentHashMap特有的部分。
put
public V put(K key, V value) {
return putVal(key, value, false);
}
复制代码
ConcurrentHashMap的put方法结构和HashMap一致,都是通过Key,Value的方式去设值。这里有一处地方我们需要特别注意,在HashMap中key是有hash过后的值传入,在ConcurrentHashMap中直接作为key传入,我们需要特别关注。
接下来我们具体分析一下核心putVal,代码篇幅比较长,我们将分几段进行分析。首先看第一段:
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
复制代码
从上述代码可以看出在concurrentHashMap中是不允许key或者value为空,如果为空会抛出NullPoint的异常,同时hash值由spread方法负责Hash化key值。
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
}
复制代码
如果tab为空,就调用initTable进行初始化。这里其实开始就和HashMap有所不同了,这里出现了CasTabAt(自旋调整并插入),也就是CAS锁,这里通过一个CAS算法实现了无锁化的插入。
这里先简单介绍一下CAS锁。
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
说到CAS又不得不提一个经典问题ABA,这里先引出来后续填坑。
else if ((fh = f.hash) ==MOVED)
tab = helpTransfer(tab, f);
else {
复制代码
假如hash值为-1的时候会触发helpTransfer,意思就是当hash为-1的时候此时正在进行扩容,调用helpTransfer加速扩容。具体扩容算法比较复杂,这暂时不做过多的分析。
如果tab不为空,并且当前没有其他线程正在扩容,则利用synchronized锁写入数据。
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;
}
}
}
}
复制代码
如果大于转换界限会转换为红黑树,当count(桶数量)大于7的时候就会树化。
if (binCount >=TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
复制代码
这里有一点与JDK7有所不同,JDK8采用的是synchronized技术来锁定线程,JDK7采用分段锁技术来进行锁定线程。
为什么会有这个差别?
synchronized之前一直都是重量级的锁,但是后来java官方是对他进行过升级的,他现在采用的是锁升级的方式去做的。针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。
所以是一步步升级上去的,最初也是通过很多轻量级的方式锁定的。
参考自:www.cnblogs.com/aobing/p/12…
get
具体流程如下所示:
- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
- 如果是红黑树那就按照树的方式获取值。
- 就不满足那就按照链表的方式遍历获取值。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算Hash
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;
}
复制代码
CAS
CAS是一个乐观锁的实现方式,是一种轻量级锁,JUC中很多工具类都是基于CAS去实现的。乐观锁的具体流程是:
线程在读取数据的时候不进行加锁,在准备回写数据的时候,比较原值是否修改,如果未被修改则写回,如果已经被修改则重新读取值。
ABA问题
概述
这是乐观锁里面的一种经典问题:当前有一个变量值为A,此时线程1将其改为了B,而线程2又将其改回了A,此时判断值的线程就会认为其没有修改过。不过这问题不大,只要最终结果是正确的那么过程就变得无关紧要。
解决
解决方案就是给每一次CAS操作加上版本号,在比较值的同时进行版本号比较即可。
HashTable
从实现上来说,HashTable实现线程安全的方式是给所有的方法加上synchronized,这么做确实可以保障线程安全,但是这么做效率可就不客观了。
HashTable的将所有的数据操作都上了锁。
HashTable不允许Key或value的值为空,这一点是和HashMap有所不同的,如果此时插入了空值那么就会抛出异常。
总结
本章节的重点在于ConcurrentHashMap,这是一个非常经典的数据结构,也是作为Java开发者非常常用的数据结构,我们需要去掌握好,HashTable从本文篇幅我们就可以看出,其重要程度远远不如ConcurrentHashMap,现在的开发工作中基本不再使用,做了解即可。
参考文献
[1] www.cnblogs.com/aobing/p/12…