Java 学习:ConcurrentHashMap

HashMap 不是线程安全的,多个线程同时操作会抛出 ConcurrentModificationException 异常。ConcurrentHashMap 是 HashMap 线程安全的一个实现版本。1.8中放弃了 Segment  臃肿的设计,取而代之的是采用 Node + CAS + Synchronized 来保证并发安全进行实现,结构如下:

成员变量:

private transient volatile int sizeCtl;

// ConcurrentHashMap的最大容量 2^30
private static final int MAXIMUM_CAPACITY = 1 << 30;

// ConcurrentHashMap的默认容量 2^4
private static final int DEFAULT_CAPACITY = 16;

// hash值为-1表明线程正在扩容中,会创建hash为-1的fwd结点
static final int MOVED = -1; 

// 扩容线程所负责的区间大小最低为16,避免发生大量的内存冲突
private static final int MIN_TRANSFER_STRIDE = 16;

// 用于扩容过程中,指示原数组下一个分割区间的上界位置
private transient volatile int transferIndex;

/*
* 主要用于扩容过程中指向扩容后的新数组
* 只有当数组处于扩容过程时,nextTable才不为null;否则其他时刻,nextTable为null
*/
private transient volatile Node<K,V>[] nextTable;

// 节点数组,用于存储键值对,当第一次插入时进行初始化。
transient volatile Node<K,V>[] table;

sizeCtl 变量表示 ConcurrentHashMap 状态,它包括未初始化、初始化中、正常状态、扩容中这些状态。通过 CAS 告知其他线程 ConcurrentHashMap 的当前状态:

未初始化:

  • sizeCtl = 0:表示还没有指定容器容量
  • sizeCtl > 0:已初始化容器容量

初始化中:

  • sizeCtl = -1(正在初始化)

正常状态:

  • sizeCtl = 0.75*n(扩容阈值):表示这次扩容的阈值,每一次扩容会对桶容量 t=  n << 1,在扩容过程中,会判断 t < sizeCtl,为真则扩容成功,否则会继续扩容(n << 1,sizeCtl = 0.75*n),直到前面的判断为真或 n >= MAXIMUM_CAPACITY。

扩容中(sizeCtl < 0):

  • sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 :表示只有一个线程在扩容
  • sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + n + 2:表示有 n 个线程在扩容

构造函数

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (initialCapacity < concurrencyLevel)   // Use at least as many bins
        initialCapacity = concurrencyLevel;   // as estimated threads
    // 初始化容量和加载因子相乘
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    // 重算数组槽容量
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
    this.sizeCtl = cap;
}

插入元素

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    // 计算哈希码
    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();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            /*
            * 当前槽还没有存放结点,存放添加的结点
            * 注意存放的索引:(n-1)&hash
            */
            if (casTabAt(tab, i, null,
                    new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 槽的第一个结点作为内置锁
            synchronized (f) {
                // 获得锁
                if (tabAt(tab, i) == f) {
                    // 存放在指定槽的第一个结点引用没有改变
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                            (ek != null && key.equals(ek)))) {
                                // 存在对应的key结点
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                // 不存在key的结点,添加新结点
                                pred.next = new Node<K,V>(hash, key,
                                        value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        // 树结点
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                value)) != null) {
                            // 已存在key的树结点
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    // 如果链表结点大于等于8就树化
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

/*
* 重构哈希码
*/
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

/*
* 通过CAS获取存放指定位置的首结点
*/
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}


/*
* CAS保存到指定位置的结点引用
*/
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);
}

/*
* 把结点存放到对应的槽(存放的位置与取的位置计算相同)
*/
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

/*
* 树化操作
*/
private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1);
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            /*
            * 并发处理获取锁
            * 需要树化的链表
            */
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                        null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    /*
                    * TreeBin对象在初始化时会构造一棵红黑树
                    */
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

插入元素流程:

  1. 当前桶指向的引用为空,使用 CAS 来添加元素,不需要加锁操作。
  2. 当前桶指向的引用不为空,把指向的引用对象作为锁,执行加锁操作。也就是每个桶都有对应的锁,当多个线程对不同的桶执行插入操作时,并行执行插入。

扩容:

/*
* 扩容后桶的大小总是2的幂次方
* 初始化容量:n=16   sizeCtl=12
* 第一次扩容:n=32   sizeCtl=24
* 第三次扩容:n=64   sizeCtl=48  n<=sizeCtl 退出扩容
*/
private final void tryPresize(int size) {
    // 扩大桶的大小,必须是2的幂次方
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
    int sc;

    /*
    * sizeCtl=0表示容器没有被初始化
    * sizeCtl>0表示容器已初始化,准备扩容
    */
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
        // 容器没有被初始化,准备初始化
        if (tab == null || (n = tab.length) == 0) {
            n = (sc > c) ? sc : c;
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if (table == tab) {
                        @SuppressWarnings("unchecked")
                        // 创建桶数组
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                        // 设置扩容阈值
                        sc = n - (n >>> 2);
                    }
                } finally {
                    // 设置扩容阈值0.75*n
                    sizeCtl = sc;
                }
            }
        }
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            /* 
            * 当阈值大于需要扩容的大小时(初始化的 t = n << 1)、
            * 容器大于等于最大允许大小时成功,才退出。
            * 注意:这里的阈值不是下次需要扩容的大小,是这次扩容的阈值,它是跟初始扩大
            * 容量(n<<1)比较,小于则继续扩大容量(n<<1 sizeCtl=0.75*n)
            */
            break;
        else if (tab == table) {
            int rs = resizeStamp(n);
            if (sc < 0) {
                // 正在扩容中
                Node<K,V>[] nt;
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                    // 扩容已经完成
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    /*
                    * 当前线程在扩容时发现已存在其他的线程正在执行扩容,
                    * 则参与进去扩容任务中,不同的线程分配不同的桶的迁移任务,
                    * 并使用内置锁来处理并发执行的情况
                    */
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                    (rs << RESIZE_STAMP_SHIFT) + 2))
                /*
                * 这里设置SIZECTL的值(rs << RESIZE_STAMP_SHIFT) + 2
                * 数组槽的容量最大值是(1 << 30)=1073741824
                * 当数组槽长度大于(1 << 15)=32767,就会出现负数
                */
                transfer(tab, null);
        }
    }
}

/*
* 扩容过程中,会把旧数组的数据迁移到扩容后新数组上
* 从右到左每次从旧数组迁移stride个桶数据到新数组
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    // 计算每次迁移结点个数,不小于16
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {            // initiating
        // 容器未初始化则初始化容器
        try {
            @SuppressWarnings("unchecked")
            // 初始化的时候扩容为原来两倍大小
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        // 临时交换数组
        nextTable = nextTab;
        // 设置迁移索引
        transferIndex = n;
    }
    int nextn = nextTab.length;
     
     /*
     * 这是一个空的标志节点,当数组结点为null或被转移之后就会把数组槽引用指向ForwardingNode结点
     */
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    // 是否全部转移标志
    boolean finishing = false; // to ensure sweep before committing nextTab

    /*
    * 会对旧数组槽进项两次全局检查:
    *  1、为所有的数组元素指向ForwardingNode对象引用
    *  2、提交前再次检查是否都符合第一条规则
    */
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;

        /*
        * while循环主要设置transferIndex标志位,为了迁移旧数组结点
        */
        while (advance) {
            int nextIndex, nextBound;
            /*
            * 退出while循环:
            *  1:完成扩容
            *  2:当前线程完成自己负责的那部分区域
            */
            if (--i >= bound || finishing)
                advance = false;
            // 完成扩容
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                    (this, TRANSFERINDEX, nextIndex,
                            nextBound = (nextIndex > stride ?
                                    nextIndex - stride : 0))) {
                /*
                * 设置transferIndex-=stride,当transferIndex小于0则设为0
                * 从右到左迁移标志直到0索引位置
                */

                // 记录当前线程迁移哈希桶的最左边界
                bound = nextBound;
                // 记录当前线程迁移哈希桶的最大索引
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                // 完成扩容
                nextTable = null;
                // 设置新的数组槽
                table = nextTab;
                // 设置扩容阈值0.75*n
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                // 再次设置i,再次检查迁移是否完成,同时设置完成标志finishing为true
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            /*
            * 把数组槽中为null的元素设置为ForwardingNode结点
            * ForwardingNode结点的哈希码是MOVED(-1)
            */
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            // 当槽的引用的结点的哈希码是MOVED表明已经设置过了
            advance = true; // already processed
        else {
            // 加锁操作
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    // 原位置、新位置结点引用
                    Node<K,V> ln, hn;
                    if (fh >= 0) {
                        int runBit = fh & n;
                        // 与上一个结点的(hash & n)值不同的最近结点
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                // 记录不同的结点位置和(hash & n)值
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            // 原结点标签结束的链表
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            // 新索引结点结束的链表
                            hn = lastRun;
                            ln = null;
                        }

                        /*
                        * 拆分的链表顺序与原链表结点顺序相反
                        */
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        // 原数组索引i指向ForwardingNode对象
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {
                    // 与HashMap实现原理相同
                    }
                }
            }
        }
    }
}

多线程并发扩容控制:

链表拆分:

扩容过程中需要注意一下几点:

  1. 容器大小必须是 2 的幂次方;
  2. sizeCtl 标志在扩容中的作用:何时才算完成?
  3. 链表的拆分。

参考文章:

ConcurrentHashMap源码分析(JDK8) 扩容实现机制

ConcurrentHashMap源码分析(1.8)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值