1. 前言
HashMap不是线程安全的,在处理并发的时候可能会出现问题。而HashTable虽然是线程安全的,但是却是需要所有操作竞争同一把锁,效率自然会下降很多。而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问不同段的数据时,就不会出现锁竞争了,进而可以有效提高并发效率。这就是ConcurrentHashMap采用的分段锁思想。
2. 简介
ConcurrentHashMap是HashMap的线程安全版本,内部也是采用数组+链表+红黑树存储元素的,因为要尽可能保持并发的效率,所以代码会更复杂,成员变量也有很多不同,这些都是为了并发的效率考虑的,ConcurrentHashMap的并发效率比简单粗暴的HashTable会高很多。
3. 成员变量
// 最大表容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认表容量(16), 注:表的容量必须是2的次幂
private static final int DEFAULT_CAPACITY = 16;
// 最大可能的数组大小,toArray和相关方法需要
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 表的并发级别,未使用,是为了与以前的版本兼容而定义
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 表的负载因子,表示表中元素的稀疏程度,当表中单元格所占元素数量大于等于表的长度*负载因子,需要扩容
// 负载因子太小则元素比较拥挤,节点处链表过长,查询效率会变低;负载因子太大则表的空间利用率太低
// 因此负载因子的选取是很重要的
private static final float LOAD_FACTOR = 0.75f;
// 树化阈值,当表中一个单元格处的链表长度达到8的时候,就具备了树化的条件之一
static final int TREEIFY_THRESHOLD = 8;
// 反树化阈值,当表中一个单元格处的链表长度小于6时,会将红黑树转化为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 这个记录的是树化的条件之一,当表的长度64,就具备了树化的条件之一
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=-1,表示当前结点是FWD(forwarding)结点(该处结点已经被迁移)
static final int MOVED = -1; // hash for forwarding nodes
// 当node的hash=-2,表示当前结点已经树化,且当前结点是TreeBin结点(TreeBin结点代理操作红黑树)
static final int TREEBIN = -2; // hash for roots of trees
// node的hash=-3,临时保留的hash值,暂时用不到
static final int RESERVED = -3; // hash for transient reservations
// 0x7fffffff转化为2进制就是1111111111111111111111111111111(31个1)
// 在计算哈希值时会用到
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
// 当前系统的cpu数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
// 用来存储表中的数据,ConcurrentHashMap底层就是Node数组
transient volatile Node<K,V>[] table;
// 在数组扩容时需要用到的表,表示新表,扩容之后将新表赋给table,nextTable重新设置为null
private transient volatile Node<K,V>[] nextTable;
// 表示列表中的元素数量
private transient volatile long baseCount;
// -1 :当前有线程正在初始化table数组
// -N :高16位表示扩容的版本号,低16位表示扩容线程数(1 + n threads),n表示当前参与扩容的线程数
// =0 :在调用无参构造方法创建ConcurrentHashMap对象时,不会给sizeCtl赋值,默认为0,这样第一次初始化时表的容量为16
// >0 :
// 如果table未初始化,表示初始化时表的容量大小
// 如果table已经初始化,表示下一次扩容时表应该达到的阈值
private transient volatile int sizeCtl;
private transient volatile int transferIndex;
// 表示counterCells数组当前有无线程加锁
private transient volatile int cellsBusy;
// 可以对比LongAdder为了降低线程冲突创建的cells数组
// 因为baseCount表示表中元素数量,当有多个线程通知修改
// baseCount时,只会有一个线程会成功,冲突率太高,通过
// 创建数组分散热点,可以很好地减小冲突发生的概率,时间换空间
// 我写过一篇LongAdder的解析,大家可以去看一看啊~
private transient volatile CounterCell[] counterCells;
与HashMap相比,ConcurrentHashMap使用sizeCtl来控制扩容时需要达到的阈值
-
-1 :当前有线程正在初始化table数组
-
-N :高16位表示扩容的版本号,低16位表示扩容线程数(1 + n threads),n表示当前参与扩容的线程数
-
=0 :在调用无参构造方法创建ConcurrentHashMap对象时,不会给sizeCtl赋值,默认为0,这样第一次初始化时表的容量为16
-
**>**0 :
- 如果table未初始化,表示初始化时表的容量大小
- 如果table已经初始化,表示下一次扩容时表应该达到的阈值
4. 构造方法
// 无参的构造方法,sizeCtl未赋值,默认为0,如果调用这个构造方法创建对象,则第一次扩容时table的容量会是16
public ConcurrentHashMap() {
}
// 传入一个指定容量,虽然传入了指定容量,但实际上初始化时的容量是最小的大于等于这个数的2的次幂
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
// 下面这个方法会计算最小的大于等于initialCapacity*1.5+1的2的次幂
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
// 设置sizeCtl为计算出的cap,表示新建数组时需要new的数组的大小
this.sizeCtl = cap;
}
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
// 设置sizeCtl为默认容量DEFAULT_CAPACITY(16)
this.sizeCtl = DEFAULT_CAPACITY;
// 调用这个方法将传的Map集合插入列表中
putAll(m);
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
// 调用另一个构造方法
this(initialCapacity, loadFactor, 1);
}
// 指定初始容量,负载因子,和并发级别
// 虽然指定了初始容量,但是真正初始化时的容量确是最小的大于等于1+指定初始容量/负载因子的2的次幂
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {
// 负载因子小于等于0或初始容量小于0或并发级别小于0,直接抛出异常
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
// 计算1+初始容量/负载因子的大小
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
// 计算最小的大于等于1+初始容量/负载因子的2的次幂
MAXIMUM_CAPACITY : tableSizeFor((int)size);
// 设置sizeCtl为计算出的cap,表示创建数组时需要new的数组的大小
this.sizeCtl = cap;
}
5. 内部类
Node结点
这个节点类型就是我们存储到ConcurrentHashMap中的基本类型
// Node结点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 哈希值
final K key; // 键
volatile V val; // 值
volatile Node<K,V> next; // 下一个Node结点
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString(){ return key + "=" + val; }
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
public final boolean equals(Object o) {
Object k, v, u; Map.Entry<?,?> e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
}
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
ForwardingNode结点
这个结点在扩容的时候会用到。
// ForwardingNode结点
// 如果是一个写的线程(并发扩容),则需要为创建新表贡献一份力
// 如果是一个读的线程,则调用内部类的find(int h, Object k)方法读出数据
static final class ForwardingNode<K,V> extends Node<K,V> {
// 表示新表的引用
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
// 到新表上读数据
Node<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;) {
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
}
TreeNode结点
表示红黑树上的结点。
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;
TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
Node<K,V> find(int h, Object k) {
return findTreeNode(h, k, null);
}
/**
* Returns the TreeNode (or null if not found) for the given key
* starting at given root.
*/
final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
if (k != null) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk; TreeNode<K,V> q;
TreeNode<K,V> pl = p.left, pr = p.right;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.findTreeNode(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
}
return null;
}
}
TreeBin结点
代为管理红黑树
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
}