ConcurrentHashMap——JAVA1.8实现

381 阅读5分钟

前言

ConcurrentHashMap是支持并发的键值对容器,在JAVA1.7版本采用了分段锁的思想来编写,段与段之间可以并发修改,相同的段必须持有锁处理。而1.8进行了改变,采用了CAS和锁的方法,底层还是使用了数组+链表,hash冲突大于8的时候,链表会转换为红黑树提升查询效率。

正文

内部重要属性

transient volatile Node<K,V>[] table;

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

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

table是由Node组成的数组,下面三个方法是CAS获取指定索引的Node值和修改数组索引上值的方法

private transient volatile Node<K,V>[] nextTable;

nextTable用于扩容时候的临时表,当扩容完成后会变为null

//默认初始容量
private static final int DEFAULT_CAPACITY = 16;
//最大数组长度
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//转换为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
//退化成链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//CPU数量
static final int NCPU = Runtime.getRuntime().availableProcessors();

以上是一些静态变量

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

Node保存了key的hash值

static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }

ForwardingNode是在某个节点完成扩容后填充的,hash设置为MOVED表示已经被移到新表,内部存储了新表的位置。

*表初始化和大小调整控制。如果为负,则表正在初始化或调整大小:-1表示初始化,否则-(1 +活动的调整大小线程数)。否则,当table为null时,保留创建时要使用的初始表大小,或者默认为0。初始化之后,保留下一个元素计数值,以在该值上调整表的大小。 * /

 private transient volatile int sizeCtl;

按照官方的注释进行翻译,可以得到如下解释:

  • 当sizeCtl为-1的时候,表示此时正在有线程进行初始化
  • 为-N的时候,表示有多少个线程在进行扩容
  • 其他值表示table初始化大小

根据对后面代码的分析,发现-N是错误的表示,正确应该是二进制的低16位-1

nt rs = resizeStamp(n);

(rs << RESIZE_STAMP_SHIFT) + 2

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

private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

这是相关的方法

Integer.numberOfLeadingZeros(n)是获取最高位1之前有多少个0,一个int类型有32位,例如15的二进制是1111,最高位的1是第4个,32-4=28,这方法返回28。所以这方法的取值范围是[0,32]

在将计算出来的数和 2^15区或,那么取值范围值就为[ 2^15, $2^15+32],最后再将左移16位+2,还是一个负值。各位可以试一试,不管resizeStamp(int n)的n取到多大,例如2^31的大小,计算出来的值还是一个负数并且很小。


构造方法不在赘述,首先看一下put方法

首先看第一部分

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
	//计算hash
        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) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
	    //如果这个节点是MOVED,表示正在扩容
	    else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            }

首先是两种最简单的情况,为初始化表和对应的数组上是空值

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();
	    //尝试将sizectl设置为-1 表示正在初始化
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
		    //再次验证一遍table是否为空
                    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;
			//将容量设置为75%
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

以上是初始化table的代码

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
	//再次判断这个节点符合条件
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            int rs = resizeStamp(tab.length);
	    //1
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
		//2
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }
  1. 这里就是之前讲到的resizeStamp方法,根据表的长度计算出一个16位的数,高16位代表了当前容量
  2. 这里会将sc进行+1,表示正在扩容的线程数+1,然后进入扩容代码transfer
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
	//将 table长度 / 8 / cpu的个数  如果小于16就让他为16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE;
	//如果刚开始扩容,nextTab就为空,创建一个新的nextTable,容量为以前的两倍
        if (nextTab == null) {            
            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;
	创建一个ForwardingNode将nextTab保存进去
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false;

以上是transfer的第一段代码,这里判断了nextTab是否为空,为空则创建一个,并且设置转换的下标为原数组的长度

//通过for自循环处理每个索引中node,默认advace为true,初始化i和bound值,i指当前处理的槽位序号,bound指需要处理的槽位边界,先处理槽位15的节点
for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
		//--i表示下一个待处理的bucket 如果它>=bound 表示当前线程已经分配过bucket区域
                if (--i >= bound || finishing)
                    advance = false;
		//表示已经备份完毕了
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
		//通过CAS来修改,区间[0, 15]
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }

第一部分

  //当前线程已经处理完了
  if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
		//扩容完成 将sizeCtl设置为原数组的 两倍 * 0.75
                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;
                }
//如果原tab对应的index是空的,直接设置姐弟拿
  else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
//如果这个节点是fwd节点 设置advance为true
            else if ((fh = f.hash) == MOVED)
                advance = true;

关于这个CAS的解析

nt rs = resizeStamp(n);

(rs << RESIZE_STAMP_SHIFT) + 2

 if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;

而这个if判断做的就是上面代码的逆向操作,当完成了操作扩容后首先将线程数量减一,如果等式不等表示还有其他线程在扩容,就return;如果当前线程是最后一个扩容的线程就设置finishing的值为true进入收尾工作

else {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            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);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        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;
                                }
                            }
                            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);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }

这一段代码有点长,分为两种情况:

  • 如果是链表节点:因为扩容的长度是原长度二倍,所以一个链表上的节点就有两种情况:维持不变和i+n,第一段代码就是将一个链表分为两个链表,分别移动到i和i+n的索引位置,注意i+n的顺序是逆序的。
  • 如果是红黑树:步骤差不多,也会生成两个链表,但是会判断分出的红黑树长度,如果小于阈值就转换为普通链表。

++红黑树的实现前面文章有讲过,细节上有不一样的地方,逻辑还是这个逻辑。++


前半段看到了put操作遇到表未初始化、插入的索引位置为空且正在扩容的情况,下半段则是hash冲突的情况

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)))) {      
                        oldVal = e.val;                          
                        if (!onlyIfAbsent)                       
                            e.val = value;                       
                        break;                                   
                    }                                            
                    Node<K,V> pred = e;    
		    //向节点尾部添加                      
                    if ((e = e.next) == null) {                  
                        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) {
                    oldVal = p.val;                              
                    if (!onlyIfAbsent)                           
                        p.val = value;                           
                }                                                
            }                                                    
        }                                                        
    }        
    //如果节点数量超过阈值 转换为红黑树                                                    
    if (binCount != 0) {                                         
        if (binCount >= TREEIFY_THRESHOLD)                       
            treeifyBin(tab, i);                                  
        if (oldVal != null)                                      
            return oldVal;                                       
        break;                                                   
    }                                                            

当hash冲突的时候,会再次判断节点是否被移动,然后就是对链表或是红黑树的简单操作,最后再判断链表长度是否\geq8从而转换为红黑树,整个操作都是自旋的,直到节点添加成功为止使用break退出循环

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

get就是计算hash值找到相应的位置,如果是链表就遍历,红黑树调用node的find方法来获取指定节点。

public V remove(Object key) {
        return replaceNode(key, null, null);
    }

final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode());
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            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 {

remove的方法在前半部分和put有点像,同样在节点是MOVED情况会调用helpTransfer从而触发transfer

else {
    V oldVal = null;
    boolean validated = false;
    synchronized (f) {
        if (tabAt(tab, i) == f) {
            if (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;
                        if (cv == null || cv == ev ||
                            (ev != null && cv.equals(ev))) {
                            oldVal = ev;
			    //如果替换的值不为空则设置
                            if (value != null)
                                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;
                    if (cv == null || cv == pv ||
                        (pv != null && cv.equals(pv))) {
                        oldVal = pv;
                        if (value != null)
                            p.val = value;
			//如果removeTreeNode返回值true,表示红黑树太小
                        else if (t.removeTreeNode(p))
                            setTabAt(tab, i, untreeify(t.first));
                    }
                }
            }
        }
    }
    if (validated) {
        if (oldVal != null) {
            if (value == null)
                addCount(-1L, -1);
            return oldVal;
        }
        break;
    }
}

删除节点的主要逻辑也是分为链表和红黑树。对于链表则遍历,当找到对应的节点时,如果需要替换的value为空表示要删除这个节点,如果被删除的节点前后都有节点则连接他们,如果没有前驱节点设置当前节点为他的后置节点。红黑树删除是红黑树的方法,如果删除后树的节点不够,则需要转换为链表。


在删除节点和新增节点都会调用addCount方法来修改计数

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))) {
		 // 多线程修改baseCount修改失败时,会调用fullAddCount,把x的值插入到counterCell类中
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
	    //计算总数
            s = sumCount();
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
	   //当总结点数大于sizeClt的阈值 表示需要扩容
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (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);
                s = sumCount();
            }
        }
    }

addCount在新增节点的时候会触发检查节点数量,如果超过了sizeClt会扩容,扩容分为两种情况:其他线程正在扩容和当前线程首次扩容,可以看到当线程开始扩容的情况应对了之前的讲述:

rs << RESIZE_STAMP_SHIFT) + 2)

会讲sizeClt修改为这样,后续的扩容线程只需对sizeClt+1

 final long sumCount() {
        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;
    }

最后看一下计算节点数量的代码,在baseCount的基础上记录CounterCell

总结

经过部分源码的查看,总结出来了ConcurrentHashMap的以下特点:

  • 使用CAS和synchronized来进行同步,在synchronized的优化下性能已经很高,而synchronized仅仅对数组索引进行加锁,能够实现分段访问修改。
  • 链表+红黑树,提高hash冲突后访问性能。
  • 使用baseCount和CounterCell[]来记录节点数量,避免了多个线程共同修改baseCount进行的加锁和自旋导致的性能下降。