ConcurrentHashMap源码
1. 类概述
1.1 概述
- ConcurrentHashMap底层是散列表+红黑树实现的,与HashMap一样
- 支持高并发的检索和更新
- 线程安全并且检索操作不会加锁
- 检索出来的结果是最新设置的值
- 一些统计方法,例如
size()
、isEmpty()
、containsValue()
,最好是在单线程的环境下使用,不然它只满足监控或估算的目的,在项目中 (多环境下)使用它是无法准确返回的 - 当有太多的散列碰撞时,该表会动态增长
- 再散列 (扩容) 是一件非常耗费资源的操作,最好是提前计算放入容量中有多少的元素来手动初始化负载因子和初始容量,这样会好很多
- 当很多的key的HashCode相等时会非常影响性能 (散列冲突),key实现Comparable接口 (自定义比较key),会好一点
- 能被用来频繁改变的Map,通过LongAdder
- 实现了Map和Iterator的所有方法
- ConcurrentHashMap不允许key或value为null
- ConcurrentHashMap提供方法支持批量操作
1.2 ConcurrentHashMap与HashTable的区别
- HashTable是在每个方法上都加上了synchronized完成同步,效率低下
- ConcurrentHashMap通过在部分加锁和利用CAS算法来实现同步
2. CAS算法
2.1 定义
CAS (compare and swap),比较与交换,是一种有名的无锁算法。
2.2 描述
CAS中有3个操作数
- 内存值V
- 旧的预期值A
- 要修改的新值B
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值(A和内存值V相同时,将内存值V修改为B),而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试 (否则什么都不做)
因此,CAS算法的过程为,先比较是否相等,如果相等则替换。
3. volatile关键字
3.1 定义
- volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性
- 保证该变量对所有线程的可见性
- 在多线程的环境下,当这个变量修改时,所有的线程都会知道该变量被修改了,也就是所谓的“可见性”
- 不保证原子性
- 修改变量 (赋值) 实际上是在JVM中分多步执行的,而在这几步中, (从装载变量到修改),它是不安全的
4. ConcurrentHashMap源码
4.1 属性
//最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
//默认容量
private static final int DEFAULT_CAPACITY = 16;
//最大数组大小
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//默认并发级别
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//负载因子,默认0.75
private static final float LOAD_FACTOR = 0.75f;
//树化阈值
static final int TREEIFY_THRESHOLD = 8;
//链化阈值
static final int UNTREEIFY_THRESHOLD = 6;
//最小树化容量
static final int MIN_TREEIFY_CAPACITY = 64;
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
//hash特殊值
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
static final int NCPU = Runtime.getRuntime().availableProcessors();
//哈希表
transient volatile Node<K,V>[] table;
//只在扩容时不为空的表
private transient volatile Node<K,V>[] nextTable;
//基础计数器
private transient volatile long baseCount;
/**
* 散列表初始化和扩容都是由这个变量来控制!
* 当它为负数时,它正在被初始化或者扩容
* -1:表示正在初始化
* -N:表示N-1个线程在进行扩容
* 默认是0;
* 初始化之后,保存着下一次扩容的大小
*/
private transient volatile int sizeCtl;
//分割表时用的索引值
private transient volatile int transferIndex;
//计算size使用
private transient volatile int cellsBusy;
//计算size使用
private transient volatile CounterCell[] counterCells;
//视图
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
4.2 构造方法
//创建一个空的map,默认初始化表大小为16
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);
}
//concurrencyLevel:估计更新并发数量
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;
}
4.3 put方法
//向外暴露的put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//计算出key的hash值处理后的值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
//f:当前节点
//n:哈希表长度
//i:要插入的哈希表下标
//fh:目标节点的哈希值(标记值)
Node<K,V> f; int n, i, fh;
//当哈希表为空时,初始化哈希表
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// no lock when adding to empty bin
//当插入目标位置为空时,构建新节点插入(不加锁)
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
//当前节点处于移动中
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//当前节点有值时,使用加锁操作
synchronized (f) {
//再次确定当前节点是否与判断前节点一致
if (tabAt(tab, i) == f) {
//fh大于0,则当前节点处于空闲状态
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//当前节点与被插入节点key值相同时,替换value
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;
//将新节点插入二叉树中,若已存在该key值,则判断是否替换
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash,key,value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//做put后续操作
if (binCount != 0) {
//当bincount大于树化阈值,将该链表树化
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
4.4 initTable方法
全程保持只有一个线程对哈希表进行初始化操作
//初始化哈希表,使用sizeCtl的大小
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//多线程使用该方法时进行初始化
while ((tab = table) == null || tab.length == 0) {
//当sizeCtl小于0时,表明有线程在操作该哈希表,线程等待
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//无线程占用
//比较并替换SIZECTL为sc
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")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//设置sizeCtl(扩容阈值)为哈希表长度的0.75
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
4.5 get方法
//向外暴露的get方法,非同步
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//h:key对应的hash值
int h = spread(key.hashCode());
//存在当前key所存储的节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//若该节点hash值和key相等,则返回
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;
}
在ConcurrentHashMap中,Node中的value和next字段使用了volatile关键字,使得每一次get操作获取到的都是它最新的值。
5. 总结
- 底层结构是散列表 (数组+链表)+红黑树,这一点和HashMap是一样的
- Hashtable是将所有的方法进行同步,效率低下。而ConcurrentHashMap作为一个高并发的容器,它是通过部分锁定+CAS算法来进行实现线程安全的。CAS算法也可以认为是乐观锁的一种
- 在高并发环境下,统计数据 (计算size等等) 其实是无意义的,因为在下一个,size值就发生了彼岸花
- get方法是非阻塞,无锁的。重写Node类,通过volatile修饰next来实现每次获取都是最新设置的值
- ConcurrentHashMap的key和value都不能为null