Java 8 ConcurrentHashMap解析

Q: ConcurrentHashMap需要满足什么样的需求(也就是解决了什么样的问题)
A: ConcurrentHashMap首先是一个map,所以有基本的put, get方法,当然也会有size方法等,但是put和get是最常用也是最重要的。ConcurrentHashMap和普通Map不一样的地方是,它解决了多个线程同时进行put或者get的时,可能带来的临界区冲突(race condition)的问题。考虑以下几个场景:

  • Map的有个键值对为1->"one",线程A在将其修改为1->"two",线程B在线程A后开始start,但是线程B在线程A进行真正写入前读到了尚未修改的1->"one",而不是原本希望的1->"two"。此时,我们期望的是线程A开始进行put操作之后,线程B再进行get,得到的就是更新之后的值。也就是说put操作是一个原子操作。
  • 一个空map,线程A和B同时对其进行写入,线程A写入0到10000的偶数,线程B写入1到10000的奇数,最终结果map的size小于10000。原因是在HashTable的实现中,内部有一个成员变量是size,每一次的put的时候会进行++size,而这并不是一个原子操作,所以可能会出现A和B先后拿到size的旧值,然后分别在size上加1。同时这个size会用来判断HashTable中用于存放键值对的数组是否需要扩容。如果size比实际put进去的元素少,那么扩容就不会及时进行。往一个已经占满的键值对数组里面进行put,新的值会链接在数组所在元素的后面,以单链表的形式。

K: put, get, size, 多线程访问不会出错
R:
Q: 如何满足put, get?如何满足多线程时put及get不出错?

Q: ConcurrentHashMap如何实现put功能?
A: 要实现put,首先要有一个ConcurrentHashMap。即需要创建一个HashMap。其创建代码:

ConcurrentHashMap map = new ConcurrentHashMap<>();

内部实现为:

public ConcurrentHashMap() {
    }

可以看到其实什么都没有做。还有另外一种方式是:

ConcurrentHashMap map = new ConcurrentHashMap<>(2);
//实现
public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }

这里也并没有进行具体的创建,只是对成员变量sizeCtl进行了赋值。
那么,具体的创建是在什么地方呢?

map.put("one", "1");

ConcurrentHashMap会在第一次进行put的时候判断是否已经创建了具体的table,如果没有就进行创建。

      ...
        for (Node[] tab = table;;) {
            Node f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
      ...

其中initTable,就是具体创建的地方:

    /**
     * Initializes table, using the size recorded in sizeCtl.
     */
    private final Node[] initTable() {
        Node[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {   // table为this.table
            if ((sc = sizeCtl) < 0)                                        // 初始化时,sizeCtl的值为0或者正数
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {    // 将SIZECTL也就是sizeCtl的值赋值为-1,此时如果有别的线程进入的话,compareAndSwapInt会返回false,然后由于sizeCtl是volatile,此时该线程再次访问sizeCtl时就是最新的值-1,就执行上面的if代码,该线程进行yield()
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node[] nt = (Node[])new Node[n];  // 此处进行HashMap的创建,也就是创建一个Node的数组
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;  将sizeCtl的值赋为sc,也就是Node数组的长度
                }
                break;
            }
        }
        return tab;
    }

实际上,ConcurrentHashMap是使用一个Node的数组来存放所有的数据,这个Node的定义:

 static class Node implements Map.Entry {
        final int hash;
        final K key;
        volatile V val;
        volatile Node next;

        Node(int hash, K key, V val, Node next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }
...

其实是一个单向链表,next指向了下一个节点。

上面的compareAndSwapInt是Java的CAS操作,具体实现根据各个操作系统而定。

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

java中表明为native的方法表示是由native的代码来实现的。也就是各个操作系统不同可能不一样。
上面的initTable的代码中,有一个成员变量sizeCtl很重要,后面的分析都会遇到。这里先看看它的具体实现:

    /**
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     */
private transient volatile int sizeCtl;

注意上面的英文注释,翻译一下就是:

  • sizeCtl主要用于控制Table的初始化和resize。
  • 当sizeCtl的值为负数的时候:-1表示在初始化,其他负数表示在resize
  • 当Table没有被初始化,还是空的时候,sizeCtl保存了用来初始化Node数组的初始长度,0表示使用默认初始长度。

那么SIZECTL又是什么呢?

    private static final long SIZECTL;
    static {
        try {
            U = sun.misc.Unsafe.getUnsafe();
            Class k = ConcurrentHashMap.class;
            SIZECTL = U.objectFieldOffset
                (k.getDeclaredField("sizeCtl"));
            ...
        } catch (Exception e) {
            throw new Error(e);
        }
    }

上面的代码可以看出SIZECTL只是sizeCtl属性对于当前对象的一个offset(偏移量),而compareAndSwapInt函数的第二个参数就是偏移量。所以U.compareAndSwapInt(this, SIZECTL, sc, -1)是将this对象偏移量为SIZECTL的值修改为-1,前提是当前值是sc。this对象偏移量为SIZECTL的位置就是sizeCtl。

那么,到这里,我们大致知道ConcurrentHashMap是怎么进行初始化的了。接下来是如何进行put的。put的时候有几种场景:

  1. put的key不存在,Node依然有足够的余量(未使用空间)。此时直接进行插入即可。
  2. put的key存在,此时新put的值应当覆盖原有的值
  3. put的key不存在,但是跟已经存在的key有hash冲突,此时新的值应当放在同一个数组位置的Node链表中
  4. put的key不存在,Node余量不足,需要扩容。

我们先来看看代码:

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());  //计算hash码
        int binCount = 0;
        for (Node[] tab = table;;) {        //将成员变量table赋值给tab
            Node f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();                        //初始化table
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {    //根据hash获取table中的值,如果为NULL,则继续。注意此时新取出的的值被赋给了变量f
                if (casTabAt(tab, i, null,
                             new Node(hash, key, value, null))) //通过CAS设置table中位置为i的地方,设置为一个新的Node,此处对应场景1
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)  // 将f.hash赋给fh,并判断是否为MOVED,MOVED的值为-1
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {                    // 对当前Node进行加锁
                    if (tabAt(tab, i) == f) {          // 获取table上位置为i的对象,判断是否等于f(引用相等)
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||      
                                     (ek != null && key.equals(ek)))) { //hash是根据传入的key计算出的hash,而e.hash是从Table里面位置为i的地方拿到的对象的hash值,如果二者相等,表明有hash冲突,此时如果key相等,则将新的val覆盖旧的val,此处对应场景2
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;   //此处有break
                                }
                                Node pred = e;
                                if ((e = e.next) == null) {  // 如果上面的if没有执行,那么将e.next赋给e,也就是到链表中的下一个节点,如果此节点不为null,则继续进行循环,直到到达链表的尾部,此时if成立,将新的Node添加到链表的尾部,此处对应场景3。注意此时binCount实际上记录了链表有多少个节点
                                    pred.next = new Node(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node p;
                            binCount = 2;
                            if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)  //如果大于TREEIFY_THRESHOLD,就进行Tree化,也就是使用Tree而不是链表来存储同一个hash值下的数据。此时的Node数组中存储的是TreeBin,由于TreeBin extends Node,所以这是可以的。一个TreeBin里面又包含了TreeNode,同时也包含了树的根节点。TreeNode extends Node,同时具有parent, left, right, prev四个指向其他TreeNode的指针。
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount); // 这个地方对应着场景4,对Node[]进行扩容,具体实现请看下个问题
        return null;
    }

注意,上面代码中当binCount >= TREEIFY_THRESHOLD的时候,会对单链表数组元素进行树化,单链表会变成一颗红黑树。具体过程就不在此多做描述了。
K: Node数组,单链表。
R:
Q: 如何实现多线程的支持?
A: 在initTable的时候通过变量sizeCtl来实现只有一个线程在进行初始化,具体实现方式是:

  • volatile,sizeCtl是一个被修饰为volatile的成员变量,也就是说线程对该值的修改发生于线程对其的读取之前。同时,每次读取sizeCtl都是最新的值。
  • compareAndSwapInt。这个函数是一个CAS操作,是一个原子操作。当线程发现对sizeCtl的赋值无法进行时,会返回false,此时就不会进行初始化。重新回到while循环之后,另一个线程将sizeCtl赋值为-1,此时该线程只能yield()了。
    关键代码:
private transient volatile int sizeCtl;
...
 if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
...

在进行put的时候:

  • 获取Node,使用tabAt函数。tabAt实际就是对值的volatile读
  • 添加新Node,使用casTabAt函数。casTabAt就是compareAndSwapObject的一层封装
  • 更新key对应的value或者处理Hash冲突,使用synchronized(f),同时在里面也使用了tabAt
    static final  Node tabAt(Node[] tab, int i) {
        return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

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

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

K: volatile读,CAS写。volatile读写。
Q: 如何对Node进行扩容?
A: ConcurrentHashMap的扩容操作,主要就是将原来的Node数组放到新建的一个容量更大的Node数组的过程。
要完成这个目标,就有几个问题:如何构建新的更大容量的Node数组?如何将原来的数据复制到新的数组?
上源码:

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        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 ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        if (check >= 0) {
            Node[] tab, nt; int n, sc;    // 创建tab, nt变量
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&  // 将tab变量赋值为类成员变量table,即具体存储值的数组
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                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;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);                     // 传入tab变量,进行扩容
                s = sumCount();
            }
        }
    }

具体扩容方法:

    /**
     * Moves and/or copies the nodes in each bin to new table. See
     * above for explanation.
     */
    private final void transfer(Node[] tab, Node[] 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,初始化,上面addCount函数中调用时一开始传入的即为NULL
            try {
                @SuppressWarnings("unchecked")
                Node[] nt = (Node[])new Node[n << 1];  //使用新的长度创建数组,n上面赋值为tab.length,此处的nt是nextTab的缩写
                nextTab = nt;   // 将新数组引用赋予参数nextTab
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab; // 将刚刚执行过赋值的nextTab赋给成员变量nextTable
            transferIndex = n;
        }
        int nextn = nextTab.length;     //nextn即表示nextTable的length,在整个代码中,n一般都表示length
        ForwardingNode fwd = new ForwardingNode(nextTab);  //新的nextTable放入ForwardingNode中
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node f; int fh;           
            while (advance) {
                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;    //nextIndex = transferIndex = n = tab.length,所以此时i=tab.length-1
                    advance = false;    //跳出while循环
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) { //第一次时i = n -1,所以没有一个条件满足
                int sc;
                if (finishing) {                           // 扩容结束,将nextTab赋给table,返回
                    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
                }
            }
            else if ((f = tabAt(tab, i)) == null) // 如果原始数组的n-1没有元素,则放入一个ForwardingNode
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED) // hash值为MOVED的Node就是一个ForwardingNode,如果当前节点是一个ForwardingNode,则表明已经被处理过了。这里是多个线程同时处理扩容的关键
                advance = true; // already processed
            else {
                synchronized (f) {                    // 加锁
                    if (tabAt(tab, i) == f) {          // f在往上数第二个else if的时候被赋值为tabAt(tab,i),此时为加锁后再次确认,i位置的Node没有被更改,如果不成立,则下面的代码不执行
                        Node ln, hn;
                        if (fh >= 0) {                    //表明f不是一个特殊节点,比如-1就是MOVED,表示是一个ForwradingNode,-2表示TreeBin,-3为Reserved,为保留值(当前没有使用)
                            int runBit = fh & n;
                            Node lastRun = f;
                            for (Node 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 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(ph, pk, pv, ln);
                                else
                                    hn = new Node(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);  //这里开始进行赋值,将ln赋给nextTab的i位置
                            setTabAt(nextTab, i + n, hn);//将hn赋给nextTab的i+n位置
                            setTabAt(tab, i, fwd); //将原来的tab的i位置赋上一个ForwardingNode
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
                            TreeBin t = (TreeBin)f;
                            TreeNode lo = null, loTail = null;
                            TreeNode hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode p = new TreeNode
                                    (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;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

你可能感兴趣的:(Java 8 ConcurrentHashMap解析)