集合框架 — ConcurrentHashMap
ConcurrentHashMap 相比于 synchronizedMap 和 Hashtable,有更好的线程并发度,更好的性能
并且 ConcurrentHashMap 在 jdk1.7 到 jdk1.8 有结构和锁的优化
一、ConcurrentHashMap(JDK1.7)
1、实现结构
-
是由
Segment数组
和HashEntry键值对
组成,和HashMap
一样,数组加链表组成,并且以 Segment 为单位加锁 -
Segment 是 ConcurrentHashMap 的一个内部类,结构如下:
static final class Segment<K,V> extends ReentrantLock implements Serializable { private static final long serialVersionUID = 2249069246763182397L; // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶 transient volatile HashEntry<K,V>[] table; transient int count; // 内部结构变化记录(遍历的时候结构改变了,快速失败) transient int modCount; // 大小 transient int threshold; // 负载因子 final float loadFactor; }
其中的
HashEntry
组成,和 HashMap非常类似,区别就是value值和链表都是volatile
修饰的,保证了获取时的可见性static final class HashEntry<K,V>{ final int hash; final K key; volatile V value; volatile HashEntry<K,V> next; HashEntry(int hash, K key, V value, HashEntry<K,V> next){ ... } }
2、保证并发安全 — 分段锁技术
ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock
。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,而是以Segment 数组
为单元加锁,可以支持Segment 数组
个数的并发数,提高了效率
3、put 和 get 方法
-
添加数据的时候,即使
HashEntry
中的value
是用volatile
修饰的,保证了可见性,但是并不能保证并发的原子性,所以 put 还是要加锁处理 第一步获取
Segment锁
的时候失败:
1)、尝试自旋获取锁
2)、如果重试次数达到最大,改为阻塞锁获取,保证能成功
- 查询的时候,因为 volatile 修饰,保证了获取的是最新数据,而且不用加锁,效率很快
二、ConcurrentHashMap(JDK1.8)
ConcurrentHashMap在1.7中,解决了并发问题(可以支持n个 Segment这么多次数的并发),但是还是存在遍历链表效率低的问题
1、实现结构
-
有和1.8HashMap相似的结构,链表大于8转红黑树
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(int hash, K key, V val, Node<K,V> next) { ... } }
2、ConcurrentHashMap 保证并发安全
-
抛弃了1.7的
Segment分段锁
-
采用了
CAS
+synchronized
来保证并发安全性
3、put 操作,并保证线程安全
-
根据
key
计算 hashCode -
判断是否需要进行初始化
-
为当前的
key
定位出的Node
节点,为空则写入数据(利用CAS
尝试写入),失败则说明发生了冲突,自旋保证成功(前面获得锁时失败,也是利用自旋保证成功) -
如果当前位置 hashCode == MOVED == -1(说明数组正在扩容),则进行扩容
static final int MOVED = -1;
-
如果都不满足,利用
synchronized锁
写入数据 -
链表数量大于8,节点大于最小树容量(64),转红黑树
4、CAS 轻量级锁
-
CAS:是乐观锁的一种实现方式,轻量级锁
操作流程,在读取数据时,比较乐观(认为并发操作并不总发生),会先读取数据、操作数据,再去判断数据是否被其他线程修改(通过判断原先的值和现在获取的值是否一致),若是修改,则重新执行读取流程(悲观相反,先判断)
-
乐观锁无法判断 ABA问题
例如原先获得的A值,还未做修改判断时,有个线程把数据A改成了B,又有个线程把B改回了A,这个时候,乐观锁做修改判断时,是认为没有被修改的(做一些记录什么的时候)
解决:加上一个其他的标识区分,例如除了获取值,再加个时间戳,版本号
5、synchronized
- synchronized 之前是重量级锁,性能较慢,1.8改成了
锁升级
- 就是先使⽤偏向锁优先同⼀线程,然后再次获锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂⾃旋,防⽌线程被系统挂起。最后如果以上都失败就升级为重量级锁
6、自旋锁
- 作用:如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗
- 缺点:线程自旋是需要消耗cup的,说白了就是让cup在做无用功,所以不适合持有锁的线程需要长时间占用锁执行同步块
7、ConcurrentHashMap get操作
和 HashMap 一样
- 根据
key
hash出 hashCode寻址,如果值在 Node数组上,直接返回值 - 如果是红黑树就按树的方式获取,时间复杂度O(logn)
- 不是红黑树就按照链表遍历获取,时间复杂度O(n)
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
// 根据key hash出hashCode寻址,如果值在 Node数组上,直接返回值
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果是红黑树就按树的方式获取,时间复杂度O(logn)
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 不是红黑树就按照链表遍历获取,时间复杂度O(n)
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;
}