参考
http://www.importnew.com/28263.html
http://ifeve.com/concurrenthashmap/
http://www.importnew.com/26049.html
https://www.cnblogs.com/yangming1996/p/8031199.html
JDK8 concurrentHashMap实现的基本原理
java8摒弃了java7 segment的概念,而是用 Node数组+链表+红黑树实现concurrentHashMap.
采用 synchronized+CAS操作来实现线程安全,看起来是hashMap的加强版。 java8中也有segment,但是是用来兼容旧版本。
JVM模型相关概念简介
原子性,可见性,有序性
Java内存模型是围绕着在并发过程中如何处理原子性,可见性和有序性这3个特征简历的。
原子性:read,load,assign,use,store,write等原子性变量操作,即基本数据类型的访问读写是具备原子性的(long,double除外),是由java内存模型直接保证的。
但是高层次的原子性要用其他方式实现(lock,synchronized等)
可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,就是可见性。java内存模型通过在变量修改后将新值同步回主存,在读取变量前刷新变量值来实现的。
java的volatile关键字能保证变量的可见性。
另外synchronized(加锁)和final()也可以实现可见性。
有序性:
synchronized关键字
可用来实现线程的原子性,可见性,有序性。
java语言中存在两种内建的synchronized语法:1、synchronized语句;2、synchronized方法。两种方法的实现不同。
同步代码块monitorenter
指令插入到同步代码块的开始位置。monitorexit
指令插入到同步代码块结束的位置。JVM需要保证每一个monitorenter
都有一个monitorexit
与之对应。
任何对象,都有一个monitor与之相关联,当monitor被持有以后,它将处于锁定状态。线程执行到monitorenter指令时,会尝试获得monitor对象的所有权,即尝试获取锁。
同步方法
synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。
volatile关键字
用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
可用来保证可见性和有序性。
CAS原理
如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,返回true。否则处理器不做任何操作,返回false。
比如当前线程比较成功后,准备更新共享变量值的时候,这个共享变量值被其他线程更改了,那么CAS函数必须返回false。
final实现可见性
下面就来说java8的concurrentHashMap实现。
首先是内部数据结构,如下
主要属性及内部结构
// table数组最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;
// table默认初始值,必须是2的幕数
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;
//树结构化所需的table的最小容量(若一个bin中太多节点,就给table扩容)
//这个值最少应该是 4 * TREEIFY_THRESHOLD ,才能避免扩容和树结构化阈值的冲突
static final int MIN_TREEIFY_CAPACITY = 64;
//每次transfer的最小值。范围被细分以便允许多线程扩容。这个值是作为避免发生超过内存的下界。最小应该是 DEFAULT_CAPACITY,即16
private static final int MIN_TRANSFER_STRIDE = 16;
//产生stamp的位数,32位数组中最小应该是6
private static int RESIZE_STAMP_BITS = 16;
// help resize的最大线程数,2^15-1
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// sizeCtl中记录size大小的偏移量,32-16 = 16
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
static final int MOVED = -1; // forwarding 的hash值
static final int TREEBIN = -2; // 根节点的hash值
static final int RESERVED = -3; // ReservationNode的hash值
static final int HASH_BITS = 0x7fffffff; // 普通node节点hash的位数
//cpu个数,在某个大小上设置限制
static final int NCPU = Runtime.getRuntime().availableProcessors();
/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
*当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
*当为0时:代表当时的table还没有被初始化
*当为正数时:表示初始化或者下一次进行扩容的大小
*/
private transient volatile int sizeCtl;
private transient volatile int sizeCtl;这个属性非常重要,取值如下
- 0:默认值
- -1:代表哈希表正在进行初始化
- 大于0:相当于 HashMap 中的 threshold,表示阈值
- 小于-1:代表有多个线程正在进行扩容
HashMap内部的核心数据结构,数组,其元素为Node类型
//存储节点(bins)的数组。懒加载。由第一次插入元素进行初始化,数组长度始终未2^n,用迭代器访问
transient volatile Node<K,V>[] table;
//第二个table容器,扩容期间为空
private transient volatile Node<K,V>[] nextTable;
//主要在没有内容的时候使用,也作为初始化期间的返回值。通过CAS策略更新
private transient volatile long baseCount;
//见前面注解
private transient volatile int sizeCtl;
//下一个table的索引(加1)用来在扩容期间切分
private transient volatile int transferIndex;
//扩容和创建CounterCells期间的自选锁
private transient volatile int cellsBusy;
/**
* Table of counter cells. When non-null, size is a power of 2.
*/,
private transient volatile CounterCell[] counterCells;
// views
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
其中的成员变量transient volatile Node<K,V>[] table对应的类型Node的定义是一个内部类,继承自Map,
也就是HashMap的容器table元素是KV实体(key-value entity),只读。 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;
...
//将用来查找节点
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;
}
}
另外,还TreeNode,继承自Node,只不过实现了红黑树结构。树结构节点用在HashMap元素(Hash冲突的元素表)头节点。当链表的节点数大于8时会转换成红黑树的结构。
static final class TreeNode<K,V> extends Node<K,V>
此外还有TreeBin,TreeBin从字面含义中可以理解为存储树形结构的容器,而树形结构就是指TreeNode,所以TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制
put操作
主要流程如下
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); //首先计算key的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)
tab = initTable(); //发现table容器为null,则初始化容器,这在首次put的时候会发生
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //用hash做下标在容器中查找
//用CAS算法(无锁)进行线程安全插入(即对比要插入位置是否为null,是null则插入)
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED) //处理扩容时的put(hash为-1)
tab = helpTransfer(tab, f); //当前正在扩容,则处理扩容
else {
//到这里说明要插入的节点存在hash冲突,则用synchronized控制来插入到链表或者红黑树
V oldVal = null;
synchronized (f) {
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)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value; //参数控制key相同的元素则覆盖value
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null); //key不相同则插入表尾
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) { //若已经满足链表元素>8,则链表转红黑树存储
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
总结一下put操作主要步骤
- 计算key的hash值,后面好几个地方要用到
- 判断当前的插入动作是不是正在扩容,是的话跳转到扩容逻辑
- 尝试插入table,CAS算法保证线程安全
- 若存在hash冲突,则尝试插入链表或者红黑树(旋转插入),整个过程用到synchronized代码块保证线程安全
- 如果当前已经满足链表节点>8,则将链表转为红黑树
- 最后检查是否已经满足扩容条件,是则扩容
initTable操作
初始化容器大小,需要配合重要属性 sizeCtl
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0) //sizeCtl <0 说明已经有线程正在初始化,则挂起当前线程
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //sc置-1表示开始初始化了,CAS保证线程安全
try {
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); //记录下次扩容大小,n*0.75
}
} finally {
sizeCtl = sc; //
}
break;
}
}
return tab;
}
扩容操作
扩容操作相对复杂,暂时不分析源码,其大致思路如下
与initTable只允许一个线程操作不同的是,扩容是允许多个线程加入的,而且特意设计成让新线程加入已提升性能。步骤如下:
当前线程检查到当前节点正在扩容时,加入扩容。final Node<K,V>[] helpTransfer()方法就是让线程加入协助扩容的。
每个加入协助扩容的线程会调用transfer方法,这个方法做的事主要是
第一步,计算当前线程能协助的迁移的最少节点数
第二步,加入到while循环去处理属于当前线程能处理的节点迁移
第三步,迁移链表或红黑树
java8中concurrentHashMap扩容的最大特色就是多线程加入协助扩容,而不是只控制并发,将其他线程拒之门外,这是设计精妙之处。
https://www.cnblogs.com/nullzx/p/8647220.html
总结
concurrentHashMap使用的并发控制方式有synchronized和CAS,
之所以用synchronized而不再用ReentrantLock,大概是因为锁的粒度降低了,由原来锁一个区间,变成现在只锁一个头节点。更多细节还需要进一步研究