一、ConcurrentHashMap1.7
1、背景
传统HashTable保证线程安全,采用synchronized锁将整个HashTable中的数组锁住,在多个线程中只允许一个线程访问Put或者Get,效率非常低。
JDK官方不推荐在多线程的情况下使用HashTable或者HashMap,建议使用ConcurrentHashMap分段HashMap,能保证效率和安全性。
2、ConcurrentHashMap1.7架构
数据结构:数组+Segments分段锁+HashEntry链表
锁的实现:Lock锁+CAS乐观锁+UNSAFE类
扩容:支持多个Segment同时扩容。默认有16个不同的Segment,每个Segment中都有自己独立的HashEntry<K,V>[] table。
第一次计算存放在哪个Segment对象中,第二次计算Segment对象中哪个HashEntry<K,V>[] table下标位置。
因为有两个数组,一个是Segment[],一个是HashEntry[],jdk7会对每一个HashEntry数组上锁。
3、核心参数
##1.无参构造函数分析: initialCapacity ---16 loadFactor HashEntry<K,V>[] table; 加载因子0.75 concurrencyLevel 并发级别 默认是16 ##2. 并发级别是能够大于2的16次方 if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; ##3.sshift 左移位的次数 ssize 作用:记录segment数组大小 int sshift = 0; int ssize = 1; while (ssize < concurrencyLevel) { ++sshift; ssize <<= 1; } ##4. segmentShift segmentMask:ssize - 1 做与运算的时候能够将key均匀存放; this.segmentShift = 32 - sshift; this.segmentMask = ssize - 1; ##5. 初始化Segment0 赋值为下标0的位置 Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]); ##6.采用CAS修改复制给Segment数组 UNSAFE.putOrderedObject(ss, SBASE, s0); // or
4、Put方法
Put方法底层的实现 简单分析
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
###计算key存放那个Segment数组下标位置;
int hash = hash(key);
int j = (hash >>> segmentShift28) & segmentMask15;
//保留最高4位与15做与运算
###使用cas 获取Segment[10]对象 如果没有获取到的情况下,则创建一个新的segment对象
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
### 使用lock锁对put方法保证线程安全问题
return s.put(key, hash, value, false);
0000 0000 00000 0000 0000 0000 0000 0011 0000 0000 00000 0000 0000 0000 0000 0011
5、深度分析
Segment<K,V> ensureSegment(int k)
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
### 使用UNSAFE强制从主内存中获取 Segment对象,如果没有获取到的情况=null
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
## 使用原型模式 将下标为0的Segment设定参数信息 赋值到新的Segment对象中
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
#### 使用UNSAFE强制从主内存中获取 Segment对象,如果没有获取到的情况=null
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
###创建一个新的Segment对象
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
###使用CAS做修改
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
###尝试获取锁,如果获取到的情况下则自旋
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
###计算该key存放的index下标位置
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
###查找链表如果链表中存在该key的则修改
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
###创建一个新的node结点 头插入法
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
###如果达到阈值提前扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
###释放锁
unlock();
}
return oldValue;
}
二、ConcurrentHashMap1.8
1、背景
ConcurrentHashMap1.7需要计算两次index值,并且Segments分段锁效率也不是很高,所以ConcurrentHashMap1.8取消分段锁,采用数组+链表+红黑树数据结构,Node节点保存数据,并基于CAS + synchronized保证node节点数据安全,将锁的粒度拆分到每个index。
数据结构:数组+链表+红黑树;直接使用Node节点来保存数据。
锁的实现:基于CAS + synchronized保证node节点数据安全,将锁的粒度拆分到每个index。
锁的竞争:多个线程同时put key的时候,多个key都落在同一个index node节点时,如果index没有发生冲突,基于CAS操作,如果发生冲突,则基于Synchronized操作。
2、源码分析
1)构造函数为空,说明是懒加载;
2)不支持key为null(put方法中)
3)binCount 记录链表长度,如果大于8的情况下,则链表转红黑树(数组容量≥64)。
4)在全局共享变量中加上volatile关键字,及时读取最新主内存数据,保证线程可见性(为第6点准备)。
5)sizeCtl 默认为0,用来控制table的初始化和扩容操作,具体应用在后续会体现出来。-1 代表一个线程正在进行table初始化扩容,-2 代表两个线程正在进行table初始化扩容(为第6点准备)。
6)ConcurrentHashMap1.8 ,每个线程做put操作初始化时,发现sizeCtl <0会进行自旋状态,非常消耗cpu,导致cpu飙升。当一个线程获取到锁至给table初始化的整个阶段,其他线程都一直在做自旋,直到判断table不为null才退出,如果占用资源的线程在给table赋值之前断电,其他线程一直自旋。
7)初始化默认长度16
8)sc 提前扩容的元素数量 sc = n - (n>>>2);
9)CAS使用:多个线程同时赋值修改没有冲突的index位置元素时使用CAS(对比第11点)
如果CAS修改成功,则直接退出自旋,否在继续自旋。
10)并发扩容时做辅助扩容,非常厉害
11)Synchronized使用 :多个线程同时赋值修改index位置冲突元素时使用Synchronized锁(对比第9点)
12)put时会对相应节点的链表进行扫描并判断,如果key相等则修改,不相等则找到最后一个元素,使用尾插法添加。
13)addCount 提前扩容;对size做++.CounterCells。记录每个线程size++的次数。
14)如何统计size?
ConcurrentHashMap1.7 每个segment有独立统计size值,可以通过累加每个segment中的size。
ConcurrentHashMap1.8,每个线程中有增加元素时,会该线程随机数与length-1取余,得出在CounterCells数组(多线程可见)中index下标位置,对其下标元素+1,最后对所有下标元素值累加得出size。当线程随机数取余产生index冲突,且多线程对该index操作时,使用cas进行操作。
3、为什么ConcurrentHashMap1.8使用Synchronized锁而不是用Lock锁?
Synchronized锁自带自旋功能,而且有锁的升级过程; Lock都没有。
4、ConcurrentHashMap1.7和1.8的区别?
① 数据结构:1.7数组+Segments分段锁+HashEntry链表;1.8数组+链表+红黑树(直接使用node存储数据)
② 锁的实现:1.7Lock锁+CAS + UNSAFE;1.8index没有发生冲突使用cas,发生冲突则使用Synchronized锁。
③ 扩容实现:1.7支持多个Segment同时扩容;1.8支持并发扩容。
5、ConcurrentHashMap为什么在构造函数初始化s0?
S0相当于模板,其他key落到其他segment上时,以S0的参数为模板初始化segment。
6、ConcurrentHashMap 底层是如何实现?
1.传统方式 使用 HashTable 保证线程问题,是采用synchronized 锁将整个HashTable 中的数组锁住, 在多个线程中只允许一个线程访问 Put 或者 Get,效率非常低,但是能够保证线程安全问题。
2.多线程的情况下 JDK 官方推荐使用 ConcurrentHashMap ConcurrentHashMap 1.7 采用分段锁设计 底层实现原理:数组+Segments 分段锁+HashEntry 链表实现 大致原理就是将一个大的 HashMap 分成 n 多个不同的小的HashTable ,不同的 key 计算 index 如果没有发生冲突 则存放到不同的小的HashTable 中,从而可以实现多线程,同时做 put 操作,但是如果多个线程同时put 操作key 发生了 index 冲突落到同一个小的 HashTable 中还是会发生竞争锁。
3.ConcurrentHashMap 1.7 采用 Lock 锁+CAS 乐观锁+UNSAFE 类里面有实现类似于 synchronized 锁的升级过程。
4.ConcurrentHashMap 1.8 版本 put 操作 取消 segment 分段设计直接使用Node数组来保存数据 index 没有发生冲突使用 cas 锁