ConcurrentHashMap源码分析

1,341 阅读13分钟

概述

对于ConcurrentHashMap的定义应该很多人都知道,是一个可以在并发环境下确保线程安全的HashMap。 下聚焦于ConcurrentHashMap几个关键点进行分析:

  1. put()如何处理?
  2. 获取size()是如何处理的?
  3. 如何扩容的?
  4. 多线程如何辅助扩容?
  5. get()如何处理?

内部关键对象及相关状态

Node

HashMap中通过一个table数组来实现,里面的每个元素是一个Entry对象(通过传入的kv封装成Entry)。 ConcurrentHashMap也有这玩意,不过名字叫Node(也实现了Map.Entry),Node中有一个next属性形成单向链表。

除了Node自身之外还有以下几种Node子类型:

  1. ForwardingNode 扩容过程中,会将老table空的位置或已经迁移的位置设置成ForwardingNode,ForwardingNode会持有一个新table的引用,用来将扩容过程中其它线程put等操作辅助其扩容(实现多线程扩容)
  2. TreeBin 红黑树, 在table[i]位置的链表元素个数超出阈值会转换成TreeBin。
  3. TreeNode 代表TreeBin树节点
  4. ReservationNode 在computeIfAbsent和compute中使用的占位节点

Node的hash值

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

上面各个常量代表各个Node子类型的hash值。除了Node自身会通过key计算hash之外,其它Node子类型的hash都是固定的:

  1. ForwardingNode的hash值是MOVED
  2. TreeBin的hash值是TREEBIN
  3. ReservationNode的hash值是RESERVED

put()如何处理?

put()方法添加一个键值对封装成Node对象到ConcurrentHashMap中, 通过key的hashcode计算出一个数组下标索引用来存储(关于如何计算之前的HashMap文章中有描述)。

在put()方法中主要关注几个重点:

  1. 如何解决hash冲突问题
  2. 如何解决多线程并发安全问题
  3. 多线程拓容的实现

秘密都在源码中,put()方法直接调用putVal()方法来处理,下面是putVal()方法的源码:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 1. 空值校验
    if (key == null || value == null) throw new NullPointerException();
    // 2. 计算key的hash值
    int hash = spread(key.hashCode());
    int binCount = 0;
    // 3. 遍历table进处理
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 3.1 校验table是否初始化,没有则进行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 3.2 计算要存储的索引位置是否为空,是则尝试cas设置node
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 3.3 当前正在扩容, 帮助扩容迁移
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 3.4 存储的位置hash冲突处理
        else {
            V oldVal = null;
            // 3.4.1 锁定table[i]桶中头节点
            synchronized (f) {
                // 3.4.1.1 二次校验头结点是否变化
                if (tabAt(tab, i) == f) {
                    // 3.4.1.1.1 当前桶中的冲突是链表结构
                    if (fh >= 0) {
                        binCount = 1;   // 计算链表元素个数,用于判断是否需要转换红黑树
                        // 3.4.1.1.1.1 循环遍历链表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 3.4.1.1.1.1.1 判断有相同key则进行修改(根据ifAbsent标识判断)
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            // 3.4.1.1.1.1.2 没有相同key将新的kv封装node并加入链表尾端
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 3.4.1.1.2 头结点是红黑树结构, 将节点put到红黑树中
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // 根据table[i]桶node个数超出TREEIFY_THRESHOLD阈值进行红黑树转换
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // size计算以及判断是否扩容
    addCount(1L, binCount);
    return null;
}

putVal()方法中的处理流程还是比较清晰的, 这里我们主要看看如何解决上面几个问题的:

  1. 当出现hash冲突,也就是计算出的table[i]位置已经有node了, 通过链表以及红黑树两种方式来处理的。(链表长度在超出阈值会转换成红黑树)
  2. 多线程安全处理主要体现在两点:
    • 判断在table[i]位置没有node时会尝试cas原子性的方式插入(在很多地方会看到这种逻辑, 先乐观的来一次cas不行再重试或者synchronize方式来处理。 梦想要有,万一成功了呢
    • table[i]位置已经有node了,会锁定头结点(synchronized(table[i]))来操作链表或红黑树(如果是红黑树table[i]存储的是TreeBin一整棵树的引用,而不是单个树节点)锁定头结点来确保线程安全, 可以在后续逻辑体现,如链表:所有节点新增都是从尾部插入,头结点是没有变的
  3. 在注释3.3中((fh = f.hash) == MOVED)会辅助扩容, 这里的扩容逻辑后面详细分析。

获取size()是如何处理的?

通过size()方法获取ConcurrentHashMap中的元素个数。

对于ConcurrentHashMap元素个数只需要关注两点:

  1. 认识CounterCell
  2. 如何增加元素个数的
  3. 如何获取ConcurrentHashMap元素个数

1. 认识CounterCell

CounterCell可以理解为一个计数器, 里面维护了一个数值v。下面是它的定义:

@sun.misc.Contended static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
    
    ...
}

ConcurrentHashMap中CounterCell的作用是什么呢?

首先要说下ConcurrentHashMap通过一个baseCount来记录元素个数,baseCount通过名字也很好理解是一个基数。当我们新增或减少map中的元素时会更新baseCount。

由于ConcurrentHashMap是为并发而生的,所以在多线程环境中会先试图通过cas的方式来更新baseCount, 然而如果线程都去通过cas更新baseCount容易失败,通过自旋等方式虽然能解决,但如果线程竞争过大需要多次自旋影响性能,所以设计了CounterCell来解决这个问题。

CounterCell来干吗呢? 当baseCount更新失败之后,ConcurrentHashMap会建立一个CouterCell数组,每个线程随机生成一个数(可以理解成hash值)并根据数组的大小计算出一个下标,通过这个下标的CounterCell来帮助记录这次更新的个数。这样有效的将baseCount的竞争分散到各个CounterCount。 最终只需要ConcurrentHashMap的元素个数只需要将baseCount以及CounterCell数组中各个数值累加即可。

2. 如何增加元素个数的

其实在putVal()方法最后一句执行代码addCount()方法中就实现来这个逻辑, 下面是源码:

private final void addCount(long x, int check) {
    /*
    CounterCell[] as 用来计数,每个CounterCell维护一个值
    b 用来表示baseCount
    s 表示baseCount+x
    */
    CounterCell[] as; long b, s;
    // 1. 判断as不等于null,或者cas操作baseCount+x失败进入内部循环
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        // 1.1 判断是否执行fullAddCount
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        // 1.2 无需扩容直接返回
        if (check <= 1)
            return;
        // 1.3 再次统计元素个数
        s = sumCount();
    }
    ... // 扩容部分
}

这里代码虽然简单,里面细节还是挺多的。详细描述下:

  1. 注释1中判断as不等于null代表之前有多线程操作cas失败更新baseCount+x失败了(第一次执行这里是false)。而后面的||判断通过cas操作(baseCount+x)会在第一次执行到这个条件,如果执行成功即可完成容器元素个数的增加(baseCount会增加)。
  2. 注释1.1中(as == null || (m = as.length - 1) < 0)部分判断没有初始化as直接进入执行fullAddCount()。而后面的判断就是先计算找出下标中的CounterCell是否存在并通过Cas更新这个CounterCell的值,否则执行fullAddCount()。

fullAddCount()是CounterCell更新的核心实现,下面是其源码:

private final void fullAddCount(long x, boolean wasUncontended) {
    int h;
    // 1. 当前线程生成一个随机数作为hash值(getProbe()这个方法有点特殊,线程多次调用会生成同一个值, 但每个线程是不一样的)
    if ((h = ThreadLocalRandom.getProbe()) == 0) {
        ThreadLocalRandom.localInit();      // force initialization
        h = ThreadLocalRandom.getProbe();
        wasUncontended = true;
    }
    boolean collide = false;    // 标识是否有冲突            // True if last slot nonempty
    // 2. 死循环遍历处理
    for (;;) {
        CounterCell[] as; CounterCell a; int n; long v;
        // 2.1 验证as是否初始化
        if ((as = counterCells) != null && (n = as.length) > 0) {
            // 省略,下面单独分析
            ...
        }
        // 2.2 以cas方式更新cellsBusy来控制初始化as
        else if (cellsBusy == 0 && counterCells == as &&
                 U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
            boolean init = false;
            try {                           // Initialize table
                if (counterCells == as) {
                    CounterCell[] rs = new CounterCell[2];
                    rs[h & 1] = new CounterCell(x);
                    counterCells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        // 2.3 前面的处理都失败了,尝试更新baseCount
        else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
            break;                          // Fall back on using base
        // 再次进入循环
    }
}

上面的源码先省略了注释2.1中if的处理代码(下面会详细分析), for中的第一级ifelse控制分支还是比较容易理解的:

  1. 先看CounterCell[]是否初始化,已经初始化则进行更新CounterCell相关处理。
  2. 对CounterCell[]进行初始化(一开始数组大小是2, 并将本次初始化成功线程的x(更新元素值)作为其中的一个CounterCell初始值)。
  3. 最后由于前面的操作反正都失败了,试图去更新baseCount+x。

接下来再详细看看了注释2.1中if的省略的代码:

if ((as = counterCells) != null && (n = as.length) > 0) {
    // 1. 通过计算出的下标在CounterCell[]为null进行处理
    if ((a = as[(n - 1) & h]) == null) {
        if (cellsBusy == 0) {            // Try to attach new Cell
            // 1.1 创建CounterCell对象
            CounterCell r = new CounterCell(x); // Optimistic create
            // 1.2 通过cellsBusy控制标志,将新建的CounterCell更新到数组中
            if (cellsBusy == 0 &&
                U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                boolean created = false;
                try {               // Recheck under lock
                    CounterCell[] rs; int m, j;
                    if ((rs = counterCells) != null &&
                        (m = rs.length) > 0 &&
                        rs[j = (m - 1) & h] == null) {
                        rs[j] = r;
                        created = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                // 成功直接退出
                if (created)
                    break;
                continue;           // Slot is now non-empty
            }
        }
        collide = false;
    }
    // 2. 上次cas操作失败,更新wasUncontended再次进入for循环处理
    else if (!wasUncontended)       // CAS already known to fail
        wasUncontended = true;      // Continue after rehash
    // 3. 尝试更新对应CounterCell的值
    else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
        break;
    // 4. 当counterCells发生更新或者其长度>=CPU核心数,更新collide并再次进入for循环
    else if (counterCells != as || n >= NCPU)
        collide = false;            // At max size or stale
    // 5. 当collide并不是因为counterCells发生更新或者其长度>=CPU核心数等原因,将collide更新为true执行后续控制逻辑
    else if (!collide)
        collide = true;
    // 6. 通过cellsBusy控制状态,对CounterCell[]扩容
    else if (cellsBusy == 0 &&
             U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
        try {
            if (counterCells == as) {// Expand table unless stale
                CounterCell[] rs = new CounterCell[n << 1];
                for (int i = 0; i < n; ++i)
                    rs[i] = as[i];
                counterCells = rs;
            }
        } finally {
            cellsBusy = 0;
        }
        collide = false;
        continue;                   // Retry with expanded table
    }
    // 7. 由于上述操作都失败了,对hash再次生成并进入下一次for循环
    h = ThreadLocalRandom.advanceProbe(h);
}

上面的代码基本意图就是通过当前线程生成的hash值,对CounterCell[]中指定下标对象value进行更新。 如果指定下标没有初始化则进行初始化,否则以cas方式更新value,如果还是失败则判断CounterCell数组长度是否超出当前cpu核心数并进行拓容,然后再进行前面的处理逻辑。

这里有两个核心的标志变量:

  1. cellsBusy,当试图对CounterCell[]扩容,更新CounterCell中的value等,都要先通过cas方式更新cellsBusy成功后才能操作。
  2. collide有点不太好理解,初始默认为false。意图大致为CounterCell更新失败且数组长度没有超过cpu核心数,通过在注释5中collide标志更新为true,下次循环才有机会进入注释6扩容。

另外在注释7中,当所有ifelse分支处理完还没有执行成功,会再次生成hash值进入下一次for循环处理

size()方法实现
public int size() {
    // 通过sumCount()方法计算元素个数
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    // 将sum初始值为baseCount
    long sum = baseCount;
    if (as != null) {
        // 遍历各个CounterCell,并将value累加到sum
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

size()方法实现逻辑比较简单,通过baseCount并累加CounterCell[]中各个CounterCell的value得出最终元素个数。

如何扩容的?

扩容一词很好理解,当原容量不够的时候,扩展更多的容量来存储元素。

ConcurrentHashMap的扩容有一大亮点就是多线程辅助扩容。我们先看看扩容是如何实现的,后面再分析如何辅助扩容。

扩容的源码主要在transfer()中,下面是源码:

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    // 1. 计算步长
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    // 2. 如果新table还未创建进行相关操作
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            // 2.1 新建一个table为old的两倍
            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记录老table的长度
        transferIndex = n;
    }
    ... // 省略元素迁移代码
}

上面先省略元素迁移代码, 主要做了两件事:

  1. 计算步长,每个线程进来会根据cpu核心数以及元素个数计算步长(也就是当前线程负责迁移多少个元素, 这里指的是table数组中的元素,不是每个node链表或红黑树)
  2. 新建一个table为oldTable的两倍,并进行相关赋值。

下面看迁移是如何实现的

// 1. 新table的长度
int nextn = nextTab.length;
// 2. fwd用来将老table的put等操作转发到新table进行处理
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 3. advance标志用来判断是否往前增长
boolean advance = true;
// 4. finishing标志是否完成迁移
boolean finishing = false; // to ensure sweep before committing nextTab
// 5. for循环执行迁移处理
for (int i = 0, bound = 0;;) {
    Node<K,V> f; int fh;
    // 5.1 根据advance标志来计算nextIndex, nextBound
    while (advance) {
        /*
        bound, nextIndex, nextBound三个变量确定了当前线程要负责迁移的区间:
        1. bound表示当前线程负责迁移区间的边界(迁移是从后往前迁移的)。
        2. nextIndex是通过cas更新TRANSFERINDEX索引获得的下一次迁移下标,最后赋值给i。
        3. nextBound是通过cas更新TRANSFERINDEX索引获得的下一次边界,最后赋值给bound。
        */
        int nextIndex, nextBound;
        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))) {
            bound = nextBound;
            i = nextIndex - 1;
            advance = false;
        }
    }
    // 5.2 判断当前线程将其负责的区间迁移完成了
    if (i < 0 || i >= n || i + n >= nextn) {
        int sc;
        if (finishing) {
            nextTable = null;
            table = nextTab;
            sizeCtl = (n << 1) - (n >>> 1);
            return;
        }
        if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
            if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                return;
            finishing = advance = true;
            i = n; // recheck before commit
        }
    }
    // 5.3 oldtable下标i的位置没有元素,设置为fwd
    else if ((f = tabAt(tab, i)) == null)
        advance = casTabAt(tab, i, null, fwd);
    // 5.4 判断下标i的元素是否已经被设置为fwd
    else if ((fh = f.hash) == MOVED)
        advance = true; // already processed
    else {
        // 5.5 下标位置有元素执行具体的迁移逻辑
        synchronized (f) {
            // 5.5.1 再次校验下标i位置头结点是否发生变化
            if (tabAt(tab, i) == f) {
                Node<K,V> ln, hn;
                // 5.5.1.1 节点有有效hash值则为链表
                if (fh >= 0) {
                    // 5.5.1.1.1 按链表迁移
                    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;
                    }
                    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);
                    }
                    // 5.5.1.1.2 设置新table的i以及i*2的位置node , 并将oldtable的下标i设置为fwd
                    setTabAt(nextTab, i, ln);
                    setTabAt(nextTab, i + n, hn);
                    setTabAt(tab, i, fwd);
                    advance = true;
                }
                // 5.5.1.2 按红黑树迁移
                else if (f instanceof TreeBin) {
                    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;
                    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;
                        }
                    }
                    // 5.5.1.2.1  红黑树迁移完成后,根据阈值判断是否改回链表结构
                    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;
                    // 5.5.1.2.2 设置新table的i以及i*2的位置node , 并将oldtable的下标i设置为fwd
                    setTabAt(nextTab, i, ln);
                    setTabAt(nextTab, i + n, hn);
                    setTabAt(tab, i, fwd);
                    advance = true;
                }
            }
        }
    }
}

迁移代码的处理逻辑还是比较多的,主要做了以下几件事情:

  1. 在注释5.1的while循环中,主要是根据之前的stride步长计算出当前线程负责迁移的区间。迁移是从后往前迁的(如table长度未16,迁移是从下标15-->往下标0的方向迁移),bound为迁移区间边界,i为当前迁移的下标位置
  2. 注释5.2根据下标i判断整个迁移过程是否完成,或当前线程负责的区间是否迁移完成。
  3. 注释5.5通过synchronize锁定头结点,并按照节点类型进行相应迁移
  4. 当迁移下标的位置i完成或为null都将老table相应i的位置设置为fwd,以实现其它线程辅助迁移。

分析链表迁移细节:

// 1. runBit记录最后一个&操作不同的结果标志(看懂这里需要知道容量的计算结果始终是一个2的幂数值)
int runBit = fh & n;
// 2. 记录最后一个runBit结果不同的node,代表lastRun之后的节点runBit结果都一致
Node<K,V> lastRun = f;
// 3. 遍历链表,计算出runBit以及lastRun
for (Node<K,V> p = f.next; p != null; p = p.next) {
    int b = p.hash & n;
    if (b != runBit) {
        runBit = b;
        lastRun = p;
    }
}
// 4. 最后计算的runBit是0, 代表lastRun以及之后的node都属于原下标i的位置
if (runBit == 0) {
    ln = lastRun;
    hn = null;
}
// 5.  最后计算的runBit不等于0,代表lastRun以及之后的node都属于原下标i*2的位置
else {
    hn = lastRun;
    ln = null;
}
// 6. 遍历迁移f到lastRun之间的节点
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);
}

看懂上面的代码首先需要先知道两件事:

  1. 容量始终是一个2的幂数值
  2. oldtable下标i的位置迁移到newtable中,i只有两个值:i不变,或者i*2

上面链表迁移代码第一个for循环(注释3)为了计算出runBit以及lastRun的目的就是为了减少不必要的链表节点更新。 可以看到在注释6遍历循环,只会处理f到lastRun之间的节点。

多线程如何辅助扩容?

当调用put()等方法时, 发现当前插入位置table[i]的hash为MOVE,则辅助扩容,源码如下:

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    // 判断当前位置是ForwardingNode
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        int rs = resizeStamp(tab.length);
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            // 执行辅助扩容前,进行相关检查(如transferIndex迁移下标已经小于等于0, 或者到达辅助扩容线程的最大数量都不执行)
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            // 更新sizeCtl,进行将fwd中的新table传入并进行扩容
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

helpTransfer()辅助拓容主要是通过获取ForwardingNode中的新table,判断相关条件并操作sizeCtl成功后调用transfer()进行扩容,并将新table传入(transfer()方法会判断第二个参数新table不等于null将直接使用)。 后续transfer()处理逻辑就是计算步长,计算当前线程迁移的区间然后迁移等。

扩容小结

  1. 每个线程执行扩容时,会计算一个步长,并计算出一个需要迁移的区间进行迁移(第一个触发扩容的线程会负责创建一个oldtable两倍的新数组)。
  2. 当扩容过程中执行put等操作,通过ForwardNode实现辅助扩容。
  3. 不管是触发扩容还是辅助扩容都需要通过cas方式更新sizeCtl成功后才执行后续操作。这样也保证了新table不会重复创建。
  4. ConcurrentHashMap的put操作在发生hash冲突时,如果是链表是在尾部插入, 如果是红黑树则存储的是TreeBin(整棵树的引用),所以在迁移过程中,会锁定table[i]的头结点确保线程安全。

get()处理流程

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 1. 计算hash值
    int h = spread(key.hashCode());
    // 2. 验证table是否初始化,且通过hash计算的下标位置不能为null,否则直接返回
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 2.1 先判断table[i]的node是否相等
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 2.2 table[i]的node为红黑树,通过红黑树搜索返回
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        // 2.3 通过链表搜索返回
        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. 先比对table[i]的node是否相等
  3. 通过table[i]的node类型进行相应的搜索并返回

可以看到get()方法的处理逻辑中,完全没有synchronized, lock等相关内容。

总结

  1. ConcurrentHashMap的处理细节非常多,本文分析只是流于表面,提供一个思路。(膜拜Doug Lea)
  2. 将自己理解的部分写出来并不是一件容易的事情。
  3. 源码带来的不单单是对实现原理的认识,更体现在细节的处理、边界判断、性能的优化。 这种精神其实不管是自己练就技术,或在工作开发业务代码中都非常值得学习。