ConcurrentHashMap源码解析(下)

6. 成员方法

6.1 一些辅助方法

spread(int h)方法

这个方法是计算Node结点hash值的,在计算哈希值时将h的高位也用到,是为了使散列表更加分散

// 0x7fffffff转化为2进制就是1111111111111111111111111111111
// HASH_BITS的值就是1111111111111111111111111111111
// 这里用h^(h>>>16)是为了在计算哈希值时将h的高位也用到,使散列表更加分散
static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

tabAt(Node<K,V>[] tab, int i)方法

这个方法是为了获得table中的指定索引i处的Node结点

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);
}

casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)

通过CAS操作修改table指定位置i处的Node结点值

// c:期望值  v:要设置的结点值
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);
}

setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)

根据结点值修改table指定索引处的Node结点值

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

resizeStamp(int n)

拿到一个扩容版本号。在扩容时必须拿到扩容版本号才能进行扩容。

static final int resizeStamp(int n) {
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

tableSizeFor(int c)方法

获取最小的大于等于传入参数的2的次幂

原理就是把二进制下的数的每一位变成1之后加1,就变成了最小的大于等于c的2的次幂

// 这个方法是把c转化为二进制下的每一位都变为1,之后加上1算出的就是最小的大于等于c的2的次幂
private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

6.2 主要方法

put(K key, V value)方法

向表中插入元素

put(K key, V value)方法

public V put(K key, V value) {
    // 调用了putVal(key, value, false)方法
    return putVal(key, value, false);
}

putVal(K key, V value, boolean onlyIfAbsent)方法

// 参数key:表示添加的键
// 参数value:表示添加的值
/* 
   参数onlyIfAbsent:表示如果存在键相同时是否更新旧值为新值
			false:更新旧值为新值
            true:不更新旧值   
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 首先判断键和值是否为null,只要有一个为null,直接抛出异常(这也说明了ConcurrentHashMap的键和值都不允许为null)
    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;
        // 如果列表为null或列表长度为0说明列表还没创建,就先
        // 调用initTable()方法去创建列表,之后自旋
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 如果创建了列表,就得到当前键值对应该存储的位置是否为空,等于null表明该位置为null,直接添加
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 使用CAS操作尝试添加,添加成功退出循环,否则自旋
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 走到这里说明应该存储的位置不为空,如果存储位置处结点的hash为MOVED,表示正在迁移,去帮助扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 走到这里说明存储的位置有元素,且当前没有在扩容,则先加锁再尝试在链表或红黑树中尝试添加
        else {
            V oldVal = null;
            synchronized (f) {
               	// 二次判断
                if (tabAt(tab, i) == f) {
                    // 如果fh(f.hash)>=0,说明是链表,就尝试在链表中添加数据
                    if (fh >= 0) {
                        // binCount最小为1
                        binCount = 1;
                        // binCount记录链表中的结点个数
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果找到了
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                // 键的hash相同、键的key相同,先获取旧值
                                oldVal = e.val;
                                // 如果onlyIfAbsent为false,则更新旧值为新值
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            // 如果下一个结点为null,说明还是没有找到键相同的,就在链表结尾添加一个新的
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 如果存放位置处是一个TreeBin类型的结点,说明存储位置是一个红黑树
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        // 设置binCount为2
                        binCount = 2;
                        // 尝试在红黑树中添加结点
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            // 获取旧值
                            oldVal = p.val;
                            // 如果参数onlyIfAbsent为false,更新旧值为新值
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // 如果binCount不为0说明添加成功了或者找到了键相同的结点
            if (binCount != 0) {
                // 如果binCount大于等于树化阈值,尝试树化
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                // 如果元素存在,就返回旧值
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 统计当前表中有多少元素,并判断是否到达扩容阈值标准,如果达到了扩容阈值标准,则尝试扩容
    addCount(1L, binCount);
    return null;
}

流程如下:

  1. 如果桶数组未初始化,就先初始化

  2. 桶数组不为空,查看指定索引处的单元格是否为空,如果单元格为空,就直接插入单元格内,(成功就中断循环,失败自旋)。

  3. 如果指定索引处的单元格不为空,判断是否正在迁移元素,如果正在迁移元素,就先去帮忙扩容

  4. 如果待插入的单元格不为空不在迁移元素,就锁住这个单元格

  5. 如果当前单元格以链表形式存储,则尝试在链表中更新元素或插入元素

  6. 如果当前单元格以红黑树形式存储,则尝试在红黑树中更新元素或插入元素

  7. 如果元素存在返回旧值

  8. 如果元素不存在,列表的元素个数加1,并检查是否需要扩容,返回null

initTable()方法

初始化散列表(其实也就是创建Node数组,散列表底层就是以Node数组形式存储的)。注:在第一次put元素时,才会调用这个方法。

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    // 判断表是否还未初始化
    while ((tab = table) == null || tab.length == 0) {
        // 如果sizeCtl小于0,说明当前有其他线程正在初始化,则当前线程释放cpu
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        // 执行到这里说明没有线程在初始化表,则当前线程尝试设置sizeCtl为-1,表示自己想要初始化,
        // 如果自己设置成功,那就自己来初始化表;如果自己设置失败,表明当前有线程也在竞争设置
        // sc为-1,且人家设置成功了,那人家就会去初始化表,则自己就自旋
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                	
                if ((tab = table) == null || tab.length == 0) {
                    // 设置表容量为sizeCtl,大于0则设置为sizeCtl,小于等于0则设置为DEFAULT_CAPACITY(默认容量16)
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    // new一个Node数组
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // table指向新new的数组
                    table = tab = nt;
                    // 这个其实是计算n-n/2,也就是n*0.75
                    sc = n - (n >>> 2);
                }
            } finally {
                // 设置sizeCtl为表的容量*0.75,也就是表扩容时表中元素数量需要达到的阈值
                sizeCtl = sc;
            }
            break;
        }
    }
    // 返回新new的表
    return tab;
}

流程如下:

  1. 判断散列表是否已经初始化,没有初始化就继续往下执行已经初始化就直接返回散列表的引用table即可。

  2. 判断sizeCtl是否小于0,如果小于0,说明有线程正在初始化,当前线程释放cpu否则当前线程尝试使用CAS修改sizeCtl

    -1,表示自己想要初始化散列表,如果修改成功就往下执行。

  3. 如果sizeCtl0设置表容量为默认值(16),否则就设置表容量为sizeCtl(这时sizeCtl一定大于0)

  4. 之后new一个指定表容量大小Node数组,并将table指向这个数组

  5. 之后设置sizeCtl表容量*0.75,也就是下一次扩容时应该达到的门限值

addCount(long x, int check)方法

将散列表元素个数加1,并判断是否需要扩容

在判断扩容的时候,我们需要知道散列表中的所有元素的总数量,如果我们只使用一个BASECOUNT变量记录这个数字的话,当如果有N个线程都尝试修改这个数字的话,只会有一个线程成功,其余N-1个线程都会失败,冲突率太高,为了降低冲突,ConcurrentHashMap利用了和LongAdder一样的思想,采用counterCells数组来分散冲突,将要加的值加到counterCells数组中,冲突会小很多,在计算元素总数的时候将counterCells数组中的数和BASECOUNT加起来求得的就是散列表中的所有元素数量。

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    // 这里其实和LongAdder相较于AtomicLong的优化策略一样,采用CounterCell数组来分散baseCount这个热点数据
    // 如果counterCells数组不为空,则先尝试在counterCells数组中添加值
    // 否则尝试竞争baseCount(直接尝试在baseCount处添加值)
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        // 执行到这里说明还没有添加成功
        CounterCell a; long v; int m;
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            // 当前面三个if条件都不成立时说明CounterCell数组不为空且对应CounterCell单元格处已经创建
            // 则尝试在单元格处添加数据
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            // 如果还是失败,就执行fullAddCount方法,直接返回
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        // 计算元素个数
        s = sumCount();
    }
    
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        // 如果元素个数达到了扩容门槛,则尝试扩容
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            // rs是扩容时的版本表示
            int rs = resizeStamp(n);
            // 如果正在扩容中
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    // 扩容完成,直接退出循环
                    break;
                // 扩容未完成,则当前线程加入迁移元素中,并把扩容线程数加1
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            // 如果没有在扩容,则当前线程去扩容
            // SIZECTL高16位存储着扩容时的版本编号,低16位存储着扩容线程数+1
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                // 进入迁移元素
                transfer(tab, null);
            // 计算元素总数
            s = sumCount();
        }
    }
}

流程如下:

  1. 如果counterCells数组不为空,且指定单元格处不为空,就尝试在单元格处添加,失败执行**fullAddCount(x, uncontended)**方法,并退出循环。
  2. 利用sumCount()方法计算散列表中元素个数(将BASECOUNTcounterCells数组中所有数加起来酸楚的就是散列表中的元素个数)。
  3. 判断是否需要扩容,扩容的标准是元素个数达到SIZECTL,扩容时SIZECTL高16位存储着扩容时的版本号低16位存储着扩容线程数+1,如果正在扩容中,先判断是否扩容完成,扩容完成直接break;扩容未完成就去帮忙扩容。如果未扩容,则当前线程成为扩容的线程,去迁移元素

transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)方法

扩容方法,扩容时容量变为原来数组的两倍

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            // 扩容时容量变为原来的2倍
            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;
    // 新建一个ForwardingNode结点,并把新数组的引用存储到里面
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 整个while循环中i的值会从n-1递减到(n表示旧数组的长度),就是将旧数组中的每一个元素一步一步迁移到新数组中
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            		//nextIndex赋值transferIndex,transferIndex在上面是n
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                // 赋值nextIndex-1
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            // 如果一次遍历完成了,也就是旧数组中的所有元素都迁移完成了,就替换旧数组为新数组,并将旧数组置为null,
            // 还要重新设置sizeCtl的值,下一次扩容时需要达到的门槛值
            if (finishing) {
                // 新数组置为null
                nextTable = null;
                // 旧数组执行新创建的数组
                table = nextTab;
                // 计算下一次需要达到的门槛值(也就是新数组容量的0.75倍)
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 当前线程扩容完成,把扩容线程数-1
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                // 将finishing设置为true
                finishing = advance = true;
                // i重新设置为n
                // 这样会重新遍历一次桶数组,看看是不是都迁移完成了
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            // 如果桶中无数据,直接放入ForwardingNode结点标记该位置已经迁移
            advance = casTabAt(tab, i, null, fwd);
        // 如果桶中第一个元素的hash值为MOVED,说明它是ForwardingNode结点,也就是该桶已迁移
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            // 锁定该桶并迁移元素
            synchronized (f) {
                // 再次判断当前位置的结点是否还是原来的结点
                // 以免其他线程先一步迁移了元素
                if (tabAt(tab, i) == f) {
                    // 把一个链表分化成两个链表
                    // 规则是按照桶中各元素hash与桶大小n的与操作
                    // 等于0的放到低位链表(low)中,不等于0的放到高位链表(hn中)
                    // 其中低位链表迁移到新数组中相对位置不变
                    // 高位链表迁移到新数组中正好是旧数组中的位置+n
                    // 这也是为什么扩容时变为原来的两倍的原因
                    Node<K,V> ln, hn;
                    if (fh >= 0) {
                        // 第一个元素的哈希值大于等于0
                        // 说明该桶中元素是以链表的形式存储的
                        // 这里与HashMap迁移类似,只是多了一个lastRun
                        // lastRun是提取出链表后面不用再特殊处理的子链表
                        // 比如桶中所有元素的hash值与n与之后的值分别为 0 0 4 4 0 0 0 
                        // 则最后后面3个0对应的元素肯定还是在同一个桶中
                        // 这是lastRun对应的就是倒数第三个结点
                        int runBit = fh & 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) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        // 看一下后面几个元素属于高位链表还是低位链表
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        // 遍历链表,把hash&n为0的放到低位链表中
                        // 不为0的放到高位链表中
                        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);
                        // 放在高位链表中的元素的位置是旧数组中的位置+n
                        setTabAt(nextTab, i + n, hn);
                        // 标记当前桶已迁移
                        setTabAt(tab, i, fwd);
                        // advance为true,返回上面执行--i操作
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {
                        // 如果第一个元素是TreeBin对象,说明是一棵树
                        // 分化成两棵树
                        // 也是根据hash&n是否为0划分
                        // hash&n为0的放在低位树中
                        // hash&n不为0的放在高位树中
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        // 遍历整棵树,根据hash&n是否为0分化成两棵树
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        // 如果分化的树中的元素个数小于等于6,则退化成链表
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                            (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                            (lc != 0) ? new TreeBin<K,V>(hi) : t;
                        // 在低位树中的元素的相对位置不变
                        setTabAt(nextTab, i, ln);
                        // 在高位树的元素的位置是原位置+n
                        setTabAt(nextTab, i + n, hn);
                        // 标记该桶已迁移
                        setTabAt(tab, i, fwd);
                        // advance为true,返回上面执行--i操作
                        advance = true;
                    }
                }
            }
        }
    }
}

简要总结:

  1. 新桶数组大小是旧桶数组的两倍

  2. 迁移元素从靠后的桶开始

  3. 迁移完成的桶在里面放置一个ForwardingNode类型的变量,标记该桶迁移完成

  4. 迁移元素时根据hash&n是否等于0把桶中元素分化成两个链表或两个树

  5. 低位链表(树)存储在新数组中的相对位置不变

  6. 高位链表(树)存储在新数组中的位置是原来的位置+n

  7. 迁移元素时会当前桶,也就是分段锁的思想。

helpTransfer(Node<K,V>[] tab, Node<K,V> f)

协助扩容。线程在添加元素时发现正在扩容当前元素所在的桶元素已经迁移完成了,则协助迁移其他桶的元素

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    // 如果桶数组不为null并且表中位置存储的结点类型为ForwardingNode并且nextTable不为空
    // 说明当前桶已经迁移完成了,才会去帮忙迁移其他桶的元素
    // 扩容时会把旧桶的第一个元素设置为ForwardingNode,并且让其nextTable指向新桶数组
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        // 重新计算扩容版本号
        int rs = resizeStamp(tab.length);
        // sizeCtl < 0,说明正在扩容
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            // 扩容线程数加1
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                // 当前线程帮忙迁移元素
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

remove(Object key)方法

remove(Object key)

public V remove(Object key) {
    // 调用replaceNode(key, null, null)方法
    return replaceNode(key, null, null);
}

replaceNode(Object key, V value, Object cv)

删除元素和添加元素一样,都是先找到元素所在的桶,然后采用分段锁的思想锁住整个桶,再进行操作。

final V replaceNode(Object key, V value, Object cv) {
    // 计算hash
    int hash = spread(key.hashCode());
    // 自旋
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 如果目标所在的桶不存在,跳出循环返回null
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;
        else if ((fh = f.hash) == MOVED)
            // 如果正在扩容中,协助扩容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 标记是否处理过
            boolean validated = false;
            synchronized (f) {
                // 再次验证当前桶第一个元素是否被修改过
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        // fh >= 0表示是链表结点
                        validated = true;
                        // 遍历链表寻找目标节点
                        for (Node<K,V> e = f, pred = null;;) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                // 找到了目标节点
                                V ev = e.val;
                                // 检查目标节点的value是否等于cv
                                if (cv == null || cv == ev ||
                                    (ev != null && cv.equals(ev))) {
                                    oldVal = ev;
                                    if (value != null)
                                        // 如果value不为空则替换旧值
                                        e.val = value;
                                    else if (pred != null)
                                        // 如果前置结点不为空,删除当前节点
                                        pred.next = e.next;
                                    else
                                        // 如果前置结点为空,说明是桶中第一个元素,删除,设置新的第一元素为旧第一个元素
                                        // 的后继节点
                                        setTabAt(tab, i, e.next);
                                }
                                break;
                            }
                            pred = e;
                            // 遍历到链表结尾还没有找到元素,跳出循环
                            if ((e = e.next) == null)
                                break;
                        }
                    }
                    else if (f instanceof TreeBin) {
                        // 如果是树节点
                        validated = true;
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> r, p;
                        // 遍历树找到了目标节点
                        if ((r = t.root) != null &&
                            (p = r.findTreeNode(hash, key, null)) != null) {
                            V pv = p.val;
                            // 检查目标节点value值是否等于cv
                            if (cv == null || cv == pv ||
                                (pv != null && cv.equals(pv))) {
                                oldVal = pv;
                                if (value != null)
                                    // 如果value不为空则替换旧值
                                    p.val = value;
                                else if (t.removeTreeNode(p))
                                    // 如果value为空则删除元素
                                    // 如果删除后树的元素个数较少则退化成链表
                                    // t.removeTreeNode(p)这个方法返回true表示删除节点后树的元素个数较少
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            // 如果处理过,不管有没有找到元素都返回
            if (validated) {
                // 如果找到了元素,返回其旧值
                if (oldVal != null) {
                    // 如果要替换的值为空,元素个数减1
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    // 没找到元素返回空
    return null;
}
  1. 计算hash
  2. 如果所在的桶不存在,表示没有找到目标元素,返回
  3. 如果正在扩容,则协助扩容完成后再进行删除操作
  4. 如果是以链表形式存储的,则遍历整个链表查找元素,找到之后再删除
  5. 如果是以的形式存储的,则遍历树查找元素,找到之后再删除
  6. 如果是以的形式存储的,删除元素之后树中元素个数较小,则退化成链表
  7. 如果确实删除了元素,则整个散列表的元素数量减1,并返回旧值
  8. 如果没有删除元素返回null

get(Object key)方法

获取元素,根据目标key所在桶的第一个元素的不同采用不同的方式获取元素,关键点在于find()方法的重写

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 计算hash
    int h = spread(key.hashCode());
    // 如果元素所在的桶存在且里面有元素
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 如果第一个元素就是所要找的元素,直接返回
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0)
            // hash小于0,说明是树或者正在扩容
            // 使用find寻找元素,find的寻找方式依据Node的不同子类有不同的实现方式
            return (p = e.find(h, key)) != null ? p.val : null;
        // 遍历整个链表寻找元素
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
  1. 计算hash定位到元素所在的桶
  2. 如果桶中第一个元素就是要找的元素,直接返回
  3. 如果是或者正在迁移的元素,则调用各自Node子类的find()方法寻找元素
  4. 如果是链表遍历整个链表寻找元素

sumCount()方法

size()方法

public int size() {
    // 调用sumCount()计算元素个数
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}

sumCount()方法

元素个数的存储采用了CounterCell数组分散热点baseCount,获取元素要把baseCount和所有CounterCell数组中的数起来

final long sumCount() {
    // 计算baseCount和CounterCell数组中所有数的和
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

7. 总结

  • ConcurrentHashMap采用数组+链表+红黑树的方式存储元素

  • ConcurrentHashMap采用CAS+自旋+Synchronized方式维持并发的准确性

  • HashMap相比,ConcurrentHashMap使用sizeCtl来控制扩容时需要达到的阈值

    • -1 :当前有线程正在初始化table数组

    • -N :高16位表示扩容的版本号,低16位表示扩容线程数(1 + n threads),n表示当前参与扩容的线程数

    • =0 :在调用无参构造方法创建ConcurrentHashMap对象时,不会给sizeCtl赋值,默认为0,这样第一次初始化时表的容量为16

    • **>**0 :

      • 如果table未初始化,表示初始化时表的容量大小
      • 如果table已经初始化,表示下一次扩容时表应该达到的阈值
  • 查询操作不会加锁,所以ConcurrentHashMap不是强一致性(因为没有同步)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值