(三) 并发容器之 ConcurrentHashMap

HashMap 是线程不安全的集合, 相信只要是参加过面试的同学都会被面试官怼一句 : HashMap 线程安全吗? 为什么?

HashMap 的源码就不讲了, 直接看他的孙子 : ConcurrentHashMap (越年轻越厉害) 现在为你揭开 ConcurrentHashMap 的神秘面纱 (本章节参照的是 JDK 8 的源码, 不讨论 JDK 7 的实现)

ConcurrentHashMap

JDK8 的 ConcurrentHashMap放弃了分段锁, 并且添加了红黑树, 因为定位到节点需要进行两次运算, 效率较低, 而是采用了 Node 锁, 即每一个节点上使用 CAS+ synchronized 加锁, 并使用大量的 volatile型 变量, 锁的粒度更低了, 并发效率也更好了

有一个重要的参数sizeCtl,用于控制数组的初始化和扩容

HashMap扩容在1.8中由头插改为尾插(为了避免死循环问题),ConcurrentHashmap也是

为什么JDK8放弃了分段锁? 有什么问题?

根据官方的文档 :

  1. 加入多个分段锁浪费内存空间。
  2. 生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
  3. 为了提高 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) 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值