在java8之前ConcurrentHashMap是使用分段锁来实现并发的,数据结构为hashmap(数组加链表)的基础上再套一层segment数组,锁加在segment元素上。java8实现了粒度更细的加锁,去掉了segment数组,直接使用synchronized锁住hash后得到的数组下标位置中的第一个元素 ,如下图,这样加锁比segment加锁能支持更高的并发量。
另外,在java8中,当链表内的node数量满足一定条件时,链表会变成红黑树,加快搜索速度。
先了解几个知识点:
- 默认数组大小为16
- 扩容因子为0.75,扩容后数组大小翻倍
- 当存储的node总数量 >= 数组长度*扩容因子时,会进行扩容(数组中的元素、链表元素、红黑树元素都是内部类Node的实例或子类实例,这里的node总数量是指所有put进map的node数量)
- 当链表长度>=8且数组长度<64时会进行扩容
- 当数组下是链表时,在扩容的时候会从链表的尾部开始rehash
- 当链表长度>=8且数组长度>=64时链表会变成红黑树
- 树节点减少直至为空时会将对应的数组下标置空,下次存储操作再定位在这个下标t时会按照链表存储
- 扩容时树节点数量<=6时会变成链表
- 当一个事物操作发现map正在扩容时,会帮助扩容
- map正在扩容时获取(get等类似操作)操作还没进行扩容的下标会从原来的table获取,扩容完毕的下标会从新的table中获取
总结一下主要方法的实现原理
- put方法会首先检查table有没有初始化,没有则初始化table,定位到下标后如果下标内是代表rehash的特殊node,则会帮助扩容,否则进行更新或插入。如果链表数量达到条件则会变为红黑树。在最后增加map的总node数量时,若总数超过table长度的0.75倍则会进行扩容,扩容时会将下标倒序分为几块任务,可由其余线程帮助完成扩容
- get时会根据key定位到下标,然后遍历链表或数组查找对应的node,如果下标内是代表rehash的特殊node,则会去临时扩容table内查询数据
- remove时会根据下标遍历下标内的链表或者红黑树,如果下标内是代表rehash的特殊node,则会帮组扩容
源码分析
一.属性介绍
- table为Node数组,加锁时锁住的就是table下标内的node
- nextTable为扩容时存储扩容后node的数组
- baseCount为存储的总node数
- sizeCtl在不扩容时值为数组长度*扩容因子,扩容和初始化table时为负数
- transferIndex不扩容时为0,扩容时代表扩容的边界,扩容从数组最后一个下标倒序开始,transferIndex被赋值为数组长度,根据步长stride(根据核数计算出的,最小值为16)将扩容工作分段,将transferIndex更新transferIndex-stride,假设n为32,stride为16,则扩容任务分为下32-16、16-0两块,可由其他事物线程帮助完成。
二. 构造方法介绍
常用构造器如下
/**
* Creates a new, empty map with the default initial table size (16).
*/
public ConcurrentHashMap() {
}
/**
* Creates a new, empty map with an initial table size
* accommodating the specified number of elements without the need
* to dynamically resize.
*
* @param initialCapacity The implementation performs internal
* sizing to accommodate this many elements.
* @throws IllegalArgumentException if the initial capacity of
* elements is negative
*/
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;
}
/**
* Creates a new map with the same mappings as the given map.
*
* @param m the map
*/
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
- 默认构造器不做任何事情
- 入参有容量的构造器会将sizeCtl设置为大于容量的最小的2^n ,并不会初始化table
- 入参有map的会将sizeCtl设置为16,然后将map中的元素循环插入concurrenthashmap中,达到扩容条件会扩容
- 此外还有兼容老版本的带负载因子的构造方法,也都是只给sizeCtl赋值,不初始化table
三.常用方法(get、put、remove)
1.put
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();
//右移16位高位有效计算出的hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//初次put时,因为concurrenthashmap在构造时没有初始化table,所以先初始化table
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//n为数组长度,低位有效二次hash后计算出数组的下标,并获取该下标的node
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//如果该下标内没有node,说明此处没有hash冲突,直接将put的k-v构造为node填入此下标位置,跳出循环
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
else if ((fh = f.hash) == MOVED)
//如果下表内有node,且该node的hash值为moved(即-1),说明当前concurrenthashmap正在扩容,
//那么本次操作将会帮助扩容
tab = helpTransfer(tab, f);
else {
//node存在,且该下标位置不是扩容状态
// (扩容是一个下标一个下标进行的,此时说明当前concurrenthashmap正在扩容可能也在扩容,
// 但还没处理到该下标,该下标可以进行put操作,并且该线程不帮助扩容)
V oldVal = null;
//锁住该node(该node就是该坐标位置的第一个node)
synchronized (f) {
//再次确认下加锁前该node有没有变化
if (tabAt(tab, i) == f) {
//fh为该node的hash值,如果hash值为正数,说明该下标是按链表存储的
if (fh >= 0) {
binCount = 1;