前言
ConcurrentHashMap是concurrent包里面出镜率很高的一个类,这个类是线程安全的Map,原来jdk1.8以前的ConcurrentHashMap采用的是锁分段机制来保证线程安全。
如果关注这个锁分段技术的,可以参考这篇博文:
http://blog.csdn.net/yansong_8686/article/details/50664351
至于我,可能就不详细说锁分段机制了,这篇文章主要针对的是JDK1.8中的ConcurrentHashMap的实现方法,锁分段技术,作为一种思想,我会简单说说我的理解,我也不知道全不全面,正不正确,也是一种理解吧。
先说说锁分段技术吧
先从一个问题说起,常常在面试中,都会问有关于HashMap的相关问题:HashTable和HashMap的区别是什么?
答案一般人可能会回答HashTable是线程安全的,HashMap不是线程安全的。
OK,这没错,那HashTable现在还在用吗?
不再用了。
为什么呢?
效率低。
效率怎么低了?
如果查看底层源码的时候,就会发现,它的多线程同步处理,简直简单粗暴。就是把所有的Map中的方法加上synchronized关键字。这样一来有什么问题呢?
一个线程正在做一个什么样的操作的话,其他的N个线程都要等待,这样的多线程在线程数非常大的情况下,根本就运转不开。那咋办?除了我们自己写,在jdk1.5之后, 出现的ConcurrentHashMap就提供的一种解决思路。
可以看到,下面这张图的话,就是1.8版本以前的分段加锁的机制, 这里的ConcurrentHashMap是分成了N个Segment,这个就是片段。每个Segment又有HashEntry的数组,数组又挂着链。因此,如果加锁的话,其实也就是加一个Segment的锁,对其他的Segment没有影响。这样的做法呢,实际上是把加锁的粒度变小了。锁还是加的,还是不是全加,加一部分。
JDK1.8有什么改变
从结构图上看,它放弃了原有的Segment的结构,直接用Entry数组的方式。
当然这个图,也不是特备完整,因为在JDK1.8中,当链表长度超过8的时候,链表会转换成红黑树。当然,这不在我们的ConcurrentHashMap的讨论范围之中,我们还是主要讨论线程安全怎么保证。
要探究,底层源码跑不掉
// 截取部分的ConcurrentHashMap的源码分析
// put方法的底层是putVal方法
public V put(K key, V value) {
return putVal(key, value, false);
}
// 这个方法是不能被override
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ConcurrentHashMap是不能put null的
if (key == null || value == null) throw new NullPointerException();
// 得到key对应的hash位置
int hash = spread(key.hashCode());
// 这个主要看到底一个index下挂的有几个Node,初始当然是没有Node
int binCount = 0;
// 无线循环
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果这个table都还没有初始化过
if (tab == null || (n = tab.length) == 0)
// 初始化table
tab = initTable();
// 如果index=hash%n的那个位置Node是空
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 就把新的Node放到table的那个index处,如果是把新Node放到null的index处,不加锁。
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 如果取出的那个Node正在移动,那就帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 否则就是说呢hash为index的这个地方本来就有Node,到底是修改值,还是加链表
else {
V oldVal = null;
// 在这个地方加了锁的操作,这个粒度是table中的某个index取出来的Node,粒度很小。
synchronized (f) {
// 下面的这两个if就是再次判断的作用,是不是那个Node,是不是那个hash值
if (tabAt(tab, i) == f) {
if (fh >= 0) {
// 这个时候把binCount置为1,因为有一个Node嘛
binCount = 1;
// 这一步可以得出这个链表有多长
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果遍历的时候,找到了我们传入的key,已经在链表里有了
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// value赋为新值,并且跳出循环
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 如果遍历完了还没找到
if ((e = e.next) == null) {
// 把value变成链表的下一个Node,并跳出循环
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;
}
}
}
}
// 如果这个链表的Node的个数大于8了,转成红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 这个方法主要做了两件事,第一Map中的元素个数+1,第二判断需不需要扩容,如果需要,扩容。
addCount(1L, binCount);
return null;
}
初始化table
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
/**
* SizCtl的解释
* Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
// sc<0时,线程暂停,让CPU重新选择
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// 如果>=0,看过我Atomic类介绍的亲是不是很熟悉这个方法,正式CAS方法实现同步
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 再三确定table是空的
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
// 按照初始值16去创建Node数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 此时sc=12
sc = n - (n >>> 2);
}
} finally {
// sizeCtl=12,说明下次在size=12的时候,resize这个数组
sizeCtl = sc;
}
break;
}
}
// 返回initial之后的table
return tab;
}
get方法
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());
//根据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;
}
后记
这篇文章主要简单说了ConcurrentHashMap的安全机制,对size扩容机制并没有做详细的介绍,如果想进一步了解的朋友,可以看看参考文献中的文章,写的很好,受益良多。
参考文献
http://www.jianshu.com/p/e694f1e868ec
http://blog.csdn.net/u010723709/article/details/48007881
http://blog.csdn.net/yansong_8686/article/details/50664351