java.util.concurrent包下有众多的线程安全类实现包括:ConcurrentHashMap、ArrayBlockingQueue、ConcurrentLinkedQueue、CopyOnWriteArrayList、ThreadPoolExecutor等等。今天我们来详细介绍ConcurrentHashMap & CopyOnWriteArrayList & ThreadPoolExecutor的底层实现。
ConcurrentHashMap是多线程安全的,其底层数据与HashMap的数据结构一样。ConcurrentHashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表也可能是红黑树,用红黑树主要是为了提高查询效率。
ConcurrentHashMap成员介绍说明:
// 表的最大容量
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;
// 装载因子
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;
// 生成sizeCtl所使用的bit位数
private static int RESIZE_STAMP_BITS = 16;
// 进行扩容所允许的最大线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 记录sizeCtl中的大小所需要进行的偏移位数
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// 一系列的标识
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
//
/** Number of CPUS, to place bounds on some sizings */
// 获取可用的CPU个数
static final int NCPU = Runtime.getRuntime().availableProcessors();
//
/** For serialization compatibility. */
// 进行序列化的属性
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("segments", Segment[].class),
new ObjectStreamField("segmentMask", Integer.TYPE),
new ObjectStreamField("segmentShift", Integer.TYPE)
};
// 表
transient volatile Node<K,V>[] table;
// 下一个表
private transient volatile Node<K,V>[] nextTable;
//
/**
* Base counter value, used mainly when there is no contention,
* but also as a fallback during table initialization
* races. Updated via CAS.
*/
// 基本计数
private transient volatile long baseCount;
//
/**
* 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;
/**
* The next table index (plus one) to split while resizing.
*/
// 扩容下另一个表的索引
private transient volatile int transferIndex;
/**
* Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
*/
// 旋转锁
private transient volatile int cellsBusy;
/**
* Table of counter cells. When non-null, size is a power of 2.
*/
// counterCell表
private transient volatile CounterCell[] counterCells;
// views
// 视图
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
// Unsafe mechanics
private static final sun.misc.Unsafe U;
private static final long SIZECTL;
private static final long TRANSFERINDEX;
private static final long BASECOUNT;
private static final long CELLSBUSY;
private static final long CELLVALUE;
private static final long ABASE;
private static final int ASHIFT;
putVal函数的实现:
/** 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值通过spread方法计算而得
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)//表为空,则进行初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//表不为空并且表的长度大于0,并且该桶不为空
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))//比较并且交换值,如tab的第i项为空则生成新的node替换
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)//如果该结点的hash值为MOVED,则进行节点的转移
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null) //JDK12新逻辑
return fv;
else {
V oldVal = null;
synchronized (f) {//加锁同步
if (tabAt(tab, i) == f) {//找到tab下标为i的节点
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)))) {
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);
break;
}
}
}
else if (f instanceof TreeBin) { //如果是红黑树节点类型
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {//将hashcode,key,value放到红黑树中
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)//如果是ReservationNode这种类型直接抛错
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)//如果tree的值大于红黑树转换的阀值则进行转换
treeifyBin(tab, i);
if (oldVal != null)//旧值不为空则返回旧值
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
现在重新解释一遍putVal的过程:
- 判断存储的key,value是否为空,为空则抛出异常
- 计算key的hash值,随后进行循环,该循环可以确保成功插入数据,或table的初始化为0则进行初始化table表。
- 根据key的hash值取出table表中的元素,或取出的结点为空,则用CAS将key,value,hash值生成的结点放入桶中
- 其该结点的hash值为MOVED,则对桶中的数据进行转置。
- 对桶中的第一个节点进行加锁,对该桶进行遍历,桶中的结点的hash&value值是否与给定的hash&value值一致,不一致则进行更新,如果遍历完了依旧没有找到相等的元素,则在最后一个结点创建新的结点。
- 若binCount达到红黑树最大阀值,则将桶中的元素转换为红黑树存储,最后增加bitCount值
最后解释一下concurrenthashMap为什么快?
jdk1.7 的concurrentHashMap采用分段技术,将concurrentHashMap的容器分段存储,每一段数据分配一个segment(锁),当一个线程占用其中的一个segment的时候,其它线程可以继续访问其它的segment。segment采用的ReentrantLock实现的。
1.7的扩容是在插入之前判断table的存储元素是否超过了当前的threshhold=table.length * 0.75.concurrentHashMap扩容默认用的是table的0.75倍(居然用这个公式算0.75(n - (n >>> 2)))
jdk1.8有以下同点改进
- 采用CAS+Synchronized来保证并发的安全性问题,1.8取消了segment而用volatile修饰折table进行数据存储,这样采用table存储的数据元素作为锁,从而实现对每一行数据进行加锁,进一步减少并发冲突的概率。
- 将原来数组+链表的存储方式改为数据+链表+红黑树,对于hash表来说,最重要是能够将key hash均匀分布,对于链表个数超过为8的便转化为红黑树,查询效率由原来的O(n)提升为O(log(n)) 以改善性能。
concurrentHashMap上述的代码中可以看出在putVal中是有加锁操作的,但是读为何不用加锁呢?get的操作元素node是用volatile修饰的,volatile在同步正确的前提下,都对get可见。
1.8扩容是先插入,然后判断下一次的++size大小是否会超过当前的阀值,超过则扩容。