ConcurrentHashMap 源码分析-初始化

790 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第26天,点击查看活动详情

数组初始化时的线程安全

数组初始化时,首先会通过自旋来保证初始化成功,然后通过 CAS 设置变量的值,保证同一时刻只能有一个线程对数组进行初始化,CAS 成功之后,还会对当前数组是否已经初始化完成进行判断,如果初始化完成,就不会再次初始化,整体是通过自旋 + CAS + 双重 check 等手段保证了数组初始化时的线程安全。

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

private final Node<K,V>[] initTable() 初始化 table,通过对 sizeCtl 的变量赋值来保证数组只能被初始化一次

while ((tab = table) == null || tab.length == 0) { } 通过自旋保证初始化成功

if ((sc = sizeCtl) < 0) 小于 0 代表有线程正在初始化,释放当前 CPU 的调度权,重新发起锁的竞争

else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { } CAS 赋值保证当前只有一个线程在初始化,-1 代表当前只有一个线程能初始化,保证了数组的初始化的安全性

if ((tab = table) == null || tab.length == 0) { } 很有可能执行到这里的时候,table 已经不为空了,这里是双重 check

int n = (sc > 0) ? sc : DEFAULT_CAPACITY; 进行初始化

总结:

  1. 通过自旋死循环保证一定可以新增成功。 在新增之前,通过 for (Node<K,V>[] tab = table;;) ,用死循环的写法来保证新增一定可以成功,只要新增成功后,就可以退出当前死循环,新增失败的话,会重复继续循环,执行新增的步骤,直到新增成功为止。

  2. 当前槽点为空时,通过 CAS 新增。源码没有在判断槽点为空的情况下直接赋值,因为在判断槽点为空和赋值的这个时间段,槽点有可能已经被其他线程赋值了,所以这里采用了 CAS 的思想,能够保证槽点为空的情况下赋值成功,如果恰好槽点已经被其他线程赋值,当前 CAS 操作失败,会再次执行 for 自旋,再走槽点有值的 put 流程,这里就是自旋 + CAS 的结合。

  3. 当前槽点有值时,会锁住当前槽点。put 时,如果当前槽点有值,也就是 key 的 hash 冲突的情况,此时槽点上可能是链表或红黑树,通过锁住槽点,来保证同一时刻只会有一个线程能对槽点进行修改。

  4. 红黑树旋转时,锁住红黑树的根节点,保证同一时刻,当前红黑树只能被一个线程旋转