简介
相对JDK 1.7,ConcurrentHashMap在JDK 1.8有了很大的优化改动,底层的实现由原来的“segement数组+table数组+链表”改为了“node数组+链表或者红黑树”。
关于ConcurrentHashMap 在JDK1.7的分析,可以查看:【P说】JDK 1.7及以前ConcurrentHashMap分析
数据结构
可以看到,在JDK 1.8的时候ConcurrentHashMap在底层实现上取消了原本的segement数组,改为了“node数组+链表或者红黑树”的实现。
- 原本每一个segment是个一个ReentrantLock,现在通过CAS + synchronized来保证并发更新的安全。其中,synchronized锁的是Node数组元素,原本在1.7的时候segment是固定不变,而在JDK 1.8 Node数组是会扩张的,也就是锁的粒度会变小,减少并发冲突的概率。(synchronized的性能在1.8的时候已经有了很大的提升)
- 数组+链表+红黑树的实现,JDK 1.7的时候主要是通过链表来存放数据,在JDK 1.8的时候ConcurrentHashMap改用数组+链表+红黑树的方式,在链表长度超过8的时候会转为红黑树存储,而在红黑树中元素少于6时,又会变回链表。
成员变量
// 默认容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表节点转换红黑树节点的阈值, 8个节点转
static final int TREEIFY_THRESHOLD = 8;
// 红黑树节点转换链表节点的阈值, 6个节点转
static final int UNTREEIFY_THRESHOLD = 6;
// 转红黑树时, table的最小长度
static final int MIN_TREEIFY_CAPACITY = 64;
//链表节点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
//....
}
//红黑树节点
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
}
//树根节点
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> 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
}
Hash算法
通过两次hash获得hash值
int h = spread(key.hashCode()); //第一次hashCode
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS; //第一次获得的哈希值,异或自己的高16位。后面与上HASH_BITS是为了保证获得的是整数,HASH_BITS为二进制31个1。
}
源码分析
构造函数
什么都没做,只是初始化一些成员变量。
public ConcurrentHashMap() {
}
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;
}
Get
get方法处理比较简单,先根据hash值来确定node数组的下标,然后先判断存在node数组中的元素是不是就是我们想要的数据,如果不是,根据红黑树或者是链表,分成两种情况去查找。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode()); //获得hash值
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) { //判断在node数组的哪个下标下
if ((eh = e.hash) == h) { //判断是否当前node数组 保存的数据就是想要的数据
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;
}
Put操作
初始化node数组:
这里要先理解变量sizeCtl
的作用:
- 负数:表示进行初始化或者扩容,-1表示正在初始化,-N,表示有N-1个线程正在进行扩容
- 正数:0 表示还没有被初始化,>0的数,初始化或者是下一次进行扩容的阈值
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0) //表示由线程在进行初始化
Thread.yield(); //让出CPU执行权
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //改变sizeCtl的值为-1,表示当前线程进行初始化,因为没有拿锁,所以使用CAS操作,保证只有一个线程可以设置成功,并且返回true
try {
//初始化node数组
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2); //sc等4/3n
}
} finally {
sizeCtl = sc; //0.75n,下次扩容的阈值
}
break;
}
}
return tab;
}
插入值:
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
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 (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0) //因为构造函数中没有对node数组初始化,这里判断node数组是不是空的,然后初始化
tab = initTable(); //初始化node数组
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //如果node数组已经初始化,但是对应下标位置没有保存元素,直接新建Node对象保存
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null))) //如果node为null,这赋值的时候要使用CAS保证原子操作,因为这个时候其实没有拿锁,因此可能会有多个线程在进行,只有成功CAS的会返回true。
break;
}
else if ((fh = f.hash) == MOVED) //如果当前有其他线程在对node数组进行扩容操作,当前数组会帮忙进行扩容
tab = helpTransfer(tab, f); //帮忙扩容
else {
V oldVal = null;
synchronized (f) { //锁住对应下标的node
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)))) { //key已经存在,用新值替换旧值
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD) //保存的元素大于8,链表转为红黑树
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); // node数组扩容
return null;
}
addCount方法中,当node节点下的红黑树数据小与6时,会重新转为链表。