1、前言
我们都知道HashMap
是线程不安全的,主要体现在put和resize的时候。
使HashMap
安全,经历了HashTable
,Jdk1.7 ConcurrentHashMap
和Jdk1.8的ConcurrentHashMap
。那么他们的区别在哪里呢?
-
HashTable
:在通过使用大量的synchronized
来保证线程的同步,但这样就造成了并发低,且线程上下文的切换导致性能不太高。所以现在几乎不用HashTable了。 -
ConcurrentHashMap
(JDK1.7):这个版本的ConcurrentHashMap
采用了Segment
的形式,什么意思呢,就是给数组分段,然后给每段加锁,其本质就是通过改变锁的粒度来提高并发的性能,其思想有点类似于数据库表中的表锁和行锁。 -
ConcurrentHashMap
(JDK1.8):这个版本的ConcurrentHashMap
发生的变化很大,主要是是采用了CAS+volatile和少量的Synchronized
来保证线程的安全.(CAS即compare and swap,是JDK提供的通过硬件保证的原子性操作,而volatile是java的关键字是一种弱的轻量级同步形式,保证了内存的可见性但是没有保证原子性。所以一般是CAS+volatile一起合作保证线程安全,这个组合在很多地方都有使用,如JUC中的LOCK
)
2、源码分析
讲真,JDK1.8版本的ConCurrentHashMap的确是比较复杂的,读这个源码之前最好先把HashMap的源码读懂然后了解一下cas和volatile
虽然很复杂,但读懂后还是挺有成就感的。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());//得到hash值
int binCount = 0;
for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
ConcurrentHashMap.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 ConcurrentHashMap.Node<K,V>(hash, key, value, null)))//使用CAS操作插入,然后break;
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)//后面讲
tab = helpTransfer(tab, f);
else {//如果不为空,则需要插入到节点后面
V oldVal = null;
synchronized (f) {//进入同步块中
if (tabAt(tab, i) == f) {
if (fh >= 0) {//fh>0表示结构为链表
binCount = 1;//记录链表的长度
for (ConcurrentHashMap.Node<K,V> 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;
}
ConcurrentHashMap.Node<K,V> pred = e;
//插到尾部
if ((e = e.next) == null) {
pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof ConcurrentHashMap.TreeBin) {//否则为红黑树,调用红黑树的插入方法
ConcurrentHashMap.Node<K,V> p;
binCount = 2;
if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//节点数大于8则调用treeifybin(),这里跟HashMap不一样,具体可以看这个方法
//只有当数组长度大于64的时候才会将链表转换为红黑树,否则就扩容。
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
/**
* 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.
*/
private transient volatile int sizeCtl;
通过阅读注释我们可以了解到,这个变量其实有两个作用
-
第一个作用:是用来判断table的状态的,如果sizeCtl<0的话,表示这个tbale在经历初始化或者扩容的过程。
-
第二个作用:After initialization, holds the next element count value upon which to resize the table.初始化后,根据这个值来调整表的大小,其实就是用来判断table是否需要扩容,还记得HashMap里面的0.75*16=12吗,就是这个意思。后面会用到
-
initTable()方法
private final ConcurrentHashMap.Node<K,V>[] initTable() {
ConcurrentHashMap.Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)//如果sizeCtl小于0则代表当前有其他线程正在初始化或者扩容,这个时候放弃cpu时间片
Thread.yield();
//这里通过CAS操作让sizeCtl置为-1,(有的人看不懂的话,下面有详细讲解哦~)
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//然后就是初始化table数组了,初始容量为16,
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
ConcurrentHashMap.Node<K,V>[] nt = (ConcurrentHashMap.Node<K,V>[])new ConcurrentHashMap.Node<?,?>[n];
table = tab = nt;
//这里sc=16-4=12
sc = n - (n >>> 2);
}
} finally {
//这里sizeCtl就等于12啦
sizeCtl = sc;
}
break;
}
}
return tab;
}
-
U.compareAndSwapInt(this, SIZECTL, sc, -1)
CAS方法一共有四个参数,分别是对象,对象属性在对象中的偏移量,期望值,更改之后的值。 -
然后我们通过源码来看
SIZECTL
就懂了。
...
private static final long SIZECTL;
...
static {
try {
U = sun.misc.Unsafe.getUnsafe();
Class<?> k = ConcurrentHashMap.class;
SIZECTL = U.objectFieldOffset
(k.getDeclaredField("sizeCtl"));//得到sizeCtl在对象中的偏移量
...
} catch (Exception e) {
throw new Error(e);
}
}
- 好吧,这行代码的意思就是在通过CAS操作将对象的
sizeCtl
属性置为-1,就是告诉其他线程,本线程正在初始化,其他线程只有执行该操作的话只有暂时释放自己的时间片 -
treeifyBin()方法(将链表转化为红黑树)
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//看到没有,当数组长度小于64的时候就会先进行扩容
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));
}
}
}
}
}
private final void tryPresize(int size) {
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
//这里和initTable是一样的
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])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<K,V>[] 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(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
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;
}
暂时先分析到这里吧,后面再来修改。有问题的话欢迎批评指正。