ConcurrentHashMap底层实现原理

ConcurrentHashMap

ConcurrentHashMap最早出现在 JDK 1.5中。底层基于散列算法实现,它是一个key-value结构的容器,使用Hash算法来获取值的地址,时间复杂度是O(1)。查询非常快。

  • 是一个key-value的映射容器,key不重复
  • jdk8中的ConcurrentHashMap基于数组+链表+红黑树实现
  • 不保证键值的顺序
  • key、value都不可以存入null值
  • 线程安全。ConcurrentHashMap并非锁住整个方法,而是通过原子操作和局部加锁的方法保证了多线程的线程安全,且尽可能减少了性能损耗。

8299ff97ebb942e9b52e2a76c3816b69.png

ConcurrentHashMap的类结构图

  • 继承了AbstractMap,实现了ConcurrentMap接口,提供了key,value结构格式访问的方法
  • 实现了Serializable接口,表示HashMap支持序列化

ConcurrentHashMap的整体架构

73cc66113ee74a22b635f72a1a41095a.png

 ConcurrentHashMap在JDK1.8中的存储结构是由数组、单向链表、红黑树组成。

当我们初始化一个ConcurrentHashMap实例时,默认会初始化一个长度为16的数组。由于ConcurrentHashMap它的核心仍然是hash表,所以必然会存在hash冲突问题。

ConcurrentHashMap采用链式寻址法来解决hash冲突。

当hash冲突比较多的时候,会造成链表长度较长,这种情况会使得ConcurrentHashMap中数据元素的查询复杂度变成O(~n~)。因此在JDK1.8中,引入了红黑树的机制。

当数组长度大于64并且链表长度大于等于8的时候,单项链表就会转换为红黑树。

另外,随着ConcurrentHashMap的动态扩容,一旦链表长度小于8,红黑树会退化成单向链表。

ConcurrentHashMap的基本功能

ConcurrentHashMap本质上是一个HashMap,因此功能和HashMap一样,但是ConcurrentHashMap在HashMap的基础上,提供了并发安全的实现。

并发安全的主要实现是通过对指定的Node节点加锁,来保证数据更新的安全性。

b26315c863264a42a8e346fbb7d7cc15.png

ConcurrentHashMap在性能方面的优化

如果在并发性能和数据安全性之间做好平衡,在很多地方都有类似的设计,比如cpu的三级缓存、mysql的buffer_pool、Synchronized的锁升级等等。

ConcurrentHashMap也做了类似的优化,主要体现在以下几个方面:

  • 在JDK1.8中,ConcurrentHashMap锁的粒度是数组中的某一个节点,而在JDK1.7,锁定的是Segment,锁的范围要更大,因此性能上会更低。
  • 引入红黑树,降低了数据查询的时间复杂度,红黑树的时间复杂度是O(logn)。

ec7fd5d2f5d446f99cd79c9178a26b51.png

 当数组长度不够时,ConcurrentHashMap需要对数组进行扩容,在扩容的实现上,ConcurrentHashMap引入了多线程并发扩容的机制,简单来说就是多个线程对原始数组进行分片后,每个线程负责一个分片的数据迁移,从而提升了扩容过程中数据迁移的效率。

ConcurrentHashMap中有一个size()方法来获取总的元素个数,而在多线程并发场景中,在保证原子性的前提下来实现元素个数的累加,性能是非常低的。ConcurrentHashMap在这个方面的优化主要体现在两个点:

  • 当线程竞争不激烈时,直接采用CAS来实现元素个数的原子递增。
  • 如果线程竞争激烈,使用一个数组来维护元素个数,如果要增加总的元素个数,则直接从数组中随机选择一个,再通过CAS实现原子递增。它的核心思想是引入了数组来实现对并发更新的负载。

2c94562d44bc4981ab4fb1a6cfc5639d.png

 源码剖析

  • 成员变量

private static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量。因为32位哈希字段的前两位用于控制,所以长度为2的30次方
private static final int DEFAULT_CAPACITY = 16; //默认初始容量,一定是2的幂  (即至少1个),最多MAXIMUM_CAPACITY
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //最大数组大小。 需要toArray和相关方法 
private static final int DEFAULT_CONCURRENCY_LEVEL = 16; //默认并发级别
private static final float LOAD_FACTOR = 0.75f; //负载因子。n - (n >>> 2)
static final int TREEIFY_THRESHOLD = 8; //链表转树阈值
static final int UNTREEIFY_THRESHOLD = 6; //树退化成链表阈值
static final int MIN_TREEIFY_CAPACITY = 64; //链表进行树化的最小表容量
private static final int MIN_TRANSFER_STRIDE = 16; //每个传输步骤的最小重新绑定数。 范围被细分以允许多个调整大小线程。 此值用作下限,以避免调整大小器遇到过多的内存争用。 该值至少为DEFAULT_CAPACITY。 
private static int RESIZE_STAMP_BITS = 16; //在sizeCtl中用于生成戳记的位数。 对于32位数组,必须至少为6。 
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; //可以帮助调整大小的最大线程数。 必须符合32 - RESIZE_STAMP_BITS位。 
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; //在sizeCtl中记录尺寸戳的位移位

static final int MOVED     = -1; // hash for forwarding nodes 扩容中
static final int TREEBIN   = -2; // hash for roots of trees
static final int RESERVED  = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

static final int NCPU = Runtime.getRuntime().availableProcessors(); //cpu的数量,以设置某些大小的界限

  • put方法

public V put(@NotNull K key, @NotNull V value) {
     return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
        // key 和value不能为空
        if (key == null || value == null) throw new NullPointerException();
        // 通过spread方法,可以让高位也能参与到寻址运算
        int hash = spread(key.hashCode());
        //表示当前k-v 封装成node后插入到指定桶位后,在桶位中的所属链表的下标位置
        // 0 表示当前桶位为 null,node可以直接放着
        // 2 表示当前桶位已经树化为红黑树。
        int binCount = 0;

        // 死循环、自旋;
        // tab引用map对象的table
        for (Node<K,V>[] tab = table;;) {
            // f 表示 桶位的头节点
            Node<K,V> f;
            // n  表示散列表数组长度
            // i 表示key通过寻址计算后,得到的桶位下标
            // fn 表示桶位头节点的hash值
            int n, i, fh;
    
            // CASE1: 成立表示: 当前map中的table尚未初始化
            if (tab == null || (n = tab.length) == 0)
                // 需要初始化
                tab = initTable();

            // i表示key使用路由寻址算法得到key对应table数组的下标位置,tabAt获取指定桶位的头节点 f   
            // CASE2:
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 当前table数组i桶位是null时
                // 使用CAS方式,设置指定数组i桶位 为 Node,且期望值时null.
                // CAS 操作成功,表示ok, 直接break
                // CAS操作失败: 表示当前线程之前,有其他线程先进入向指定i桶位设置值了。
                // 当前线程只能再次自旋,去走其他逻辑。

                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }

            // CASE3:
            // 条件成立: 表示当前桶位头节点 为 FWD节点,表示当前map处于扩容中
            // 桶位的头节点,一定不是null。
            else if ((fh = f.hash) == MOVED)
                // 看到fwd节点后,当前节点有义务帮助当前map对象完成迁移数据的工作。
                tab = helpTransfer(tab, f);

            //CASE 4:
            // 当前桶位 可能是 链表 也可能是 红黑树代理节点 TreeBin
            else {
                // 当插入key存在时,会将旧值返回给oldVal,返回给put方法调用
                V oldVal = null;

                // 使用sync 加锁给"头节点",理论上是头节点
                synchronized (f) {
                    // 再次对比 看看当前桶位的头节点,是否为之前获取的头节点?
                    // 为了避免其他线程将该桶位的头节点修改掉,导致当前线程从sync加锁就有问题了,之后的操作都不用做。
                    if (tabAt(tab, i) == f) { // 条件成立,说明加锁的对象OK。
                        // 当fh >= 0; 说明当前桶位是一个普通的链表;
                        if (fh >= 0) {
                            //1.当前插入key 与链表当中所有的都不一致时,当前的插入操作是追加到链表的末尾,binCount表示链表长度。
                            //2.当前插入key与链表当中的某个元素key一致时,当前插入操作可能是替换了。binCount表示冲突位置(binCount -1 )
                            binCount = 1;

                            // 迭代循环当前桶位的链表,e是每次循环处理节点。
                            for (Node<K,V> e = f;; ++binCount) {
                                // 当前循环节点 key
                                K ek;
                                // 条件一:e.hash == hash 表示循环当前元素的hash值与插入节点的hash值一致,需要进一步判断
                                // 条件2.1: (ek = e.key) == key :  (ek != null && key.equals(ek)))
                                // 说明当前循环的节点与插入节点的key一致,发生冲突了。
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    // 将当前循环的元素的值 赋值给 oldVal
                                    oldVal = e.val;

                                    //
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }

                                // 当前元素与插入元素的key 不一致时,会走下面;
                                //1.更新循环节点为 当前节点的下一个节点
                                //2.判断下一个节点是否为 null,如果是null,说明当前节点已经是队尾了,插入数据需要追加到队尾的后面。
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        // 前置条件,该桶位一定不是链表
                        // 条件成立,表示当前桶位是红黑树代理节点TreeBin
                        else if (f instanceof TreeBin) {
                            // 当前桶位已经是红黑树
                            // p表示红黑树,如果与你插入节点的key 有冲突节点的话,则putTreeVal 方法会返回冲突节点的引用
                            Node<K,V> p;
                            // 强制设置binCount为2,因为binCount <= 1时候,会有其他含义,所以这里设置为了2.
                            binCount = 2;

                            // putTreeVal() 往红黑树插入节点
                            // 条件1成立: 说明当前插入节点的key与红黑树中的某个节点的key一致,冲突了
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                // 将冲突节点赋值给 oldVal;
                                oldVal = p.val;
                                //重写
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }

                // 说明当前桶位不为null,可能是红黑树,也可能是链表
                if (binCount != 0) {
                    // 如果binCount >= 8 表示处理的桶位一定是链表
                    if (binCount >= TREEIFY_THRESHOLD)
                        // 调用转化为链表为红黑树的方法
                        treeifyBin(tab, i);

                    // 说明当前线程插入的数据与原有key-v冲突,需要将原数据返回给调用者。
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }

        // 1.统计当前table一共有多少数据。
        //2.判断是否拿到扩容值标准,触发扩容;
        addCount(1L, binCount);
        return null;
    }

 锁Node。

  • initTable初始化方法

private final Node<K,V>[] initTable() {
        // tab 表示 map.table的引用
        Node<K,V>[] tab;
        // sc 表示临时局部的sizeCtl值
        int sc;

        // 条件是table 为 null。 当前散列表尚未初始化
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                // 大概率为 -1 ,其他线程正在进行创建table的过程,当前线程没有竞争到初始化table的锁
                Thread.yield(); // lost initialization race; just spin
            // 1.sizeCtl = 0; 表示创建table数组时 使用DEFAULT_CAPACITY为大小
            // 2.如果table未初始化,表示初始化大小;
            // 3.如果table已经初始化,表示下次扩容时的触发条件(阈值)
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    // 再次判断,防止其他线程已经初始化完毕,然后当前线程再次初始化....导致丢失数据。
                    // 如果条件成立? 说明其他线程都没有进入过这个if块,当前线程就是具备初始化table权利了。
                    if ((tab = table) == null || tab.length == 0) {

                        // sc >0 ,创建table时,使用sc为指定大小,否则使用16作为默认值
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];

                        // 最终赋值给 map.table
                        table = tab = nt;

                        // n 右移2位。 = 1/4  sc = n- 1/4n = 3/4 n = 0.75n;
                        // sc 表示下次扩容时的触发条件
                        sc = n - (n >>> 2);
                    }
                } finally {
                    // 如果当前线程是第一次创建map.table的线程的话,sc表示是下一次扩容的阈值
                    // 表示当前线程并不是第一次创建map.table 当前线程进入到else if 块时,将sizeCtl 设置为了-1,那么
                    // 这时需要将其修改为 进入时的值。
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
 }
  • casTabAt方法

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
   return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

调用sun.misc包Unsafe类的CAS方法compareAndSwapObject。

CAS(Compare-And-Swap):比较并交换。通过一个原子操作,用预期值去和实际值做对比,如果实际值和预期相同,则做更新操作。如果预期值和实际不同,就认为其他线程更新了这个值,不做更新操作。

总结

  • 做插入操作时,首先进入乐观锁,
  • 然后,在乐观锁中判断容器是否初始化,
  • 如果没初始化则初始化容器,
  • 如果已经初始化,则判断该hash位置的节点是否为空,如果为空,则通过CAS操作进行插入。
  • 如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则帮助其扩容。
  • 如果没有扩容,则进行最后一步,先加锁,然后找到hash值相同的那个节点(hash冲突),
  • 循环判断这个节点上的链表,决定做覆盖操作还是插入操作。
  • 循环结束,插入完毕。
     

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李景琰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值