HashMap 是线程不安全的集合, 相信只要是参加过面试的同学都会被面试官怼一句 : HashMap 线程安全吗? 为什么?
HashMap 的源码就不讲了, 直接看他的孙子 : ConcurrentHashMap (越年轻越厉害) 现在为你揭开 ConcurrentHashMap 的神秘面纱 (本章节参照的是 JDK 8 的源码, 不讨论 JDK 7 的实现)
ConcurrentHashMap
JDK8 的 ConcurrentHashMap放弃了分段锁, 并且添加了红黑树, 因为定位到节点需要进行两次运算, 效率较低, 而是采用了 Node 锁, 即每一个节点上使用 CAS+ synchronized 加锁, 并使用大量的 volatile型 变量, 锁的粒度更低了, 并发效率也更好了
有一个重要的参数sizeCtl,用于控制数组的初始化和扩容
HashMap扩容在1.8中由头插改为尾插(为了避免死循环问题),ConcurrentHashmap也是
为什么JDK8放弃了分段锁? 有什么问题?
根据官方的文档 :
- 加入多个分段锁浪费内存空间。
- 生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
- 为了提高 GC 的效率
使用了synchronized而不是Reentrantlock, 会导致性能有所下降.
它的内部类多的一批, 主要有三大结构 : 数组 + 链表 + 红黑树, 数组的每个元素存储的都是链表的头结点 (以下将数组元素成为 : 桶)
属性
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
// Map 可以存储的最大桶个数 ( 2^30 )
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 桶的初始容量, 必定为 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;
// 最小的红黑树容量, 最少为 TREEIFY_THRESHOLD 的四倍
static final int MIN_TREEIFY_CAPACITY = 64;
//
private static final int MIN_TRANSFER_STRIDE = 16;
// sizeCtl中用于生成标记的位数
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;
...
// 用于控制数组的初始化和扩容
private volatile int sizeCtl;
...
}
put(K, V)
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 如果 K-V 为 null, 直接抛出异常
if (key == null || value == null) throw new NullPointerException();
// Key 的 hash 码
int hash = spread(key.hashCode());
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();
// 根据 Key 的 hash 值, 定位桶的下标 (i) 并返回桶的第一个元素
// 如果桶没有存元素, 第一次插入, 则返回 null
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 使用 CAS 将创建出的 Node 对象放入桶中
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
// 检查桶中第一个节点的 hash 值, 如果为 -1
// 说明有线程正在扩容数组, 该线程加入扩容工作, 返回扩容后的数组
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 如果上面添加都不符合, 就要追加链表了
// JDK 采用节点锁, 并非分段锁
synchronized (f) {
// 再次判断, 防止多线程出现问题 (类似于 DCL 单例模式的双重检查)
if (tabAt(tab, i) == f)