ConcurrentHashMap源码分析

介绍

相对线程安全:相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。

数据结构:数组+链表+(红黑树+双向链表).与HashMap不同的是key和value不能为null. 红黑树的头节点是TreeBin,它的hash是TREEBIN = -2.

使用CAS和synchronized(链化的头节点)来保证put和扩容的线程安全。

在进行计算map中存在的映射数量的时候,使用到了类似于LongAdder的方法(int cellsBusy,CounterCell[] counterCells,long baseCount)。

在进行扩容的时候支持多个线程同时进行扩容的协助(大体思想可以联想到 redis的Hash类型进行扩容)。
ConcurrentHashMap结构图.png

源码分析

字段的含义

sizeCtl

  • sizeCtl>0

    1. table==null 未进行初始化前:保存使用赋值“初始容量”的构造方法计算得到的初始容量。

      ​ |–构造方法 2.

    2. 已经初始化:表示扩容阈值

      ​ |–initTable方法的 2.3-2.4

  • sizeCtl=0:使用默认的初始容量16。

    ​ |–initTable方法的 2.2

  • sizeCtl<0

    1. sizeCtl=-1:在initTable方法中获得sizeCtl同步标志的锁,可能进行初始化操作。

      ​ |–initTable方法的 2

    2. sizeCtl!=-1:高16位:扩容时代表容量的标识戳使用resizeStamp方法进行计算得到。低16位代表为改容量正在扩容的线程数加1。如果为1,表示已经完成扩容;如果为0,表示还未进行扩容。

nextTable

扩容时创建的新的table, 大小为oldTable的二倍. 用于进行数据迁移. 当数据完成迁移之后, 会将table=nextTable, nextTable=null. 也就是说nextTable为null, 表示没有进行扩容.

ForwardingNode

在进行扩容的时候,每个线程都会创建一个ForwardingNode. 并将完成扩容的slot指向该ForwardingNode,表示数据在进行扩容的nextTable.

注意: nextTable指向扩容的新容器, hash为 MOVED = -1.

static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null);
        this.nextTable = tab;
    }
}
//=================================================
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(int hash, K key, V val) {
            this.hash = hash;
            this.key = key;
            this.val = val;
        }

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

构造方法

//loadFactor和concurrencyLevel在此处只是计算初始容量的大小
//concurrentHashMap的负载因子为:private static final float LOAD_FACTOR = 0.75f;
public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
  //1. max(initialCapacity,concurrencyLevel)/loadFactor + 1 向下取整
        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);
  //2. 将计算得到的初始容量赋值给sizeCtl
        this.sizeCtl = cap;
    }

tabkeSizeFor方法

//得到>=c的2的次幂整数
private static final int tableSizeFor(int c) {
  //此处的运算和HashMap相同,Integer.numberOfLeadingZeros(c - 1)得到c-1的“最高阶”之前的位数
    int n = -1 >>> Integer.numberOfLeadingZeros(c - 1);
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

tabAt方法

//|--寻找table中i下标的节点
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectAcquire(tab, 
                                             //通过运算得到i下标所在内存中的偏移地址
                                             ((long)i << ASHIFT) + ABASE);
    }
//	|-- ABASE:table数组在内存中的起始地址(对象的实例数据部分)
//		  scale:Node数组中,一个元素所占的大小(2的次幂)
//		  ASHIFT:如果scale=2^n,那么ASHIFT=n
ABASE = U.arrayBaseOffset(Node[].class);
        int scale = U.arrayIndexScale(Node[].class);
        if ((scale & (scale - 1)) != 0)
            throw new ExceptionInInitializerError("array index scale not a power of two");
        ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);

为什么使用计算slot的 tabAt(Node<K,V>[] tab, int i)方法,而不是直接使用tab[i]

initTable方法

//采用自旋和CAS的方式来完成初始化,有点类似于单例模式的双重检查实现
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
      //1. 如果sizeCtl<0(sizeCtl==-1),说明有其他线程正在进行初始化工作,Thread.yield()让出cpu使用权限。
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
      //2. 这里sizeCtl作为同步标志,使用CAS将sizeCtl设置为-1表示获取到锁。
        else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
            try {
              //2.1. 判断是否在获取到锁之前,其他线程已经完成初始化。双重检查。
                if ((tab = table) == null || tab.length == 0) {
                  //2.2. sc(=sizeCtl) 等于0:使用默认的初始容量16。
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                  //2.3. 类似 n*0.75 求出扩容阈值
                    sc = n - (n >>> 2);
                }
            } finally {
              //2.4. 两种情况
              //	CASE1 在 2.1 之前其他线程已经完成初始化,此时的sizeCtl的值为扩容阈值。
              //	在获取锁的时候已经将其改为-1,需要还原,同时也有释放锁。
              //	CASE2 进行初始化,将扩容阈值赋值给sizeCtl,同时释放锁
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

spread方法

在使用spread(int h)计算hash的 时候,为什么要添加&HASH_BITS的操作,而HashMap中却没有使用?

//ConcurrentHashMap
static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
static final int HASH_BITS = 0x7fffffff;//0111 1111 1111 1111 1111 1111 1111 1111 
//HashMap
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

原因:其因为有些为负数的hash值是由特殊含义的,比如fwd节点和treebin节点。这个操作是为了让正常node节点的hash值为正数。

resizeStamp方法

//n:old-length,每一个大小的map进行扩容时有唯一的一个标识戳
static final int resizeStamp(int n) {
    return Integer.numberOfLeadingZeros(n) | 
      //为了将其变成负数
      (1 << (RESIZE_STAMP_BITS - 1));
}

putVal方法

//添加k-v,如果发生替换返回oldVal,否则返回null
final V putVal(K key, V value, boolean onlyIfAbsent) {
  //不能存储key或value为null的映射
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
  //<1> 添加k-v
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh; K fk; V fv;
      //<1.1> 调用初始化方法initTable
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
      // <1.2> 该slot==null,尝试进行CAS设置(没有发生阻塞).如果成功不再自旋,失败走其他逻辑.
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;                   // no lock when adding to empty bin
        }
      // <1.3> 前提不为null:如果当前位置被扩容成功,会将该位置设置为ForwardingNode. 说明map正在被扩容调用helpTransfer方法去帮助扩容. 然后再添加元素.
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
      //如果不允许修改,和头节点进行对比.如果相等直接将val返回
        else if (onlyIfAbsent // check first node without acquiring lock
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                 && (fv = f.val) != null)
            return fv;
      //<1.4> 对链表头节点不为null且允许替换val,和红黑树的情况进行处理
        else {
            V oldVal = null;
          //<1.4.1> 对头节点加锁
            synchronized (f) {
              //检查f是否是头节点. f在 <1.2>的时候被赋值为当时的头节点,可能在运行到此时其他线程已经完成扩容(头插法)等操作,导致f不再是头节点. 如果错误则进行自旋.
                if (tabAt(tab, i) == f) {
                  // CASE1 链表结构
                    if (fh >= 0) {
                        binCount = 1;//代表链表第几个节点,从1开始
                      //进行自旋使用尾插法插入到链尾.pred: e的前一个节点 e:当前节点(从头节点算起) 
                      //逻辑:pred=e e=e.next
                        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);
                                break;
                            }
                        }
                    }
                  // CASE2 红黑树结构
                    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;
                        }
                    }
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
          //<1.5> 判断是否需要树化:如果插入或替换操作之前链表的长度>8,并且table.length>=64,则进行树化
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
  //<2> 计算是否需要扩容
    addCount(1L, binCount);
    return null;
}

addCount方法

//计算map中的k-v映射数量,类似于LongAdder的计算方法.并决定是否进行扩容. 

private final void addCount(long x, int check) {
    CounterCell[] cs; long b, s;
  //类似于LongAdder的add方法
    if ((cs = counterCells) != null ||
        !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell c; long v; int m;
        boolean uncontended = true;
        if (cs == null || (m = cs.length - 1) < 0 ||
            (c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
      //前提counterCells!= null或对baseCount赋值的时候发生竞争
      // CASE1 check=0,slot为null并且使用CAS设置成功. 即未进行阻塞
		 // CASE2 check=1,头节点.next=null并添加,或对头节点进行替换.
        if (check <= 1)
            return;
        s = sumCount();
    }
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
      //<1> 扩容的逻辑s>=扩容阈值
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
          //<1.1> 正在进行扩容,因为已经进行过初始化的判断
            if (sc < 0) {
              // 1)对比标记戳sc的高16位是否相同(当前超过扩容阈值的容器是否是同一个容器)
              // 2)sc的低16位是线程数+1,因此当sc == rs<<16 + 1 是已经完成扩容
              // 3)进行扩容的线程达到最大值
              // 4)扩容已经结束 nextTable=null
              // 5)扩容的任务已经被分配完成(table.length=16例:有效范围1<=transferIndex<=16)
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
              //前提条件: 1)-5)不成立,sc+1: 代表扩容的线程+1.
                if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
          //<1.2> 这是进行扩容的第一个线程,注意 rs<<16+2
            else if (U.compareAndSetInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
          //<1.3> 进行计算,直到不<=扩容阈值退出while循环
            s = sumCount();
        }
    }
}

transfer方法

//从后向前进行扩容 15-0
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
  //<1> 根据cpu计算每个线程能过扩容的slot个数
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
  //<2> 第一个进行扩容的线程,扩容后的大小为原来的2倍
    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;
        }
      //<2.1> 初始化nextTable和transferIndex
        nextTable = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;
  //<3> 创建fwd节点,用于在完成slot的移动后,将oldTable中的slot设置为指向该fwd的节点.告知其已经扩容完成,到nextTable中去进行get和put操作
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;//true: 允许进行扩容任务分配 false: 不允许进行扩容分配
    boolean finishing = false; // 是否已经完成扩容
  //i: 正在进行元素转移的slot,bound和nextIndex: 分配的转移元素的slot范围[indexIndex-1,bound]. 
  //i=-1: 已经没有任务分配 或 做最后的任务的线程已经完成元素转移
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
  //<4> 如果advance为true, 继续执行未完成的任务或分配新的任务
        while (advance) {
            int nextIndex, nextBound;
          // 1) 不能进行分配的条件
          //		条件1: 还未完成已经分配的任务 条件2: 已经完成扩容 
            if (--i >= bound || finishing)
                advance = false;
          //		条件2: 已经将所有的slot分配完成无需再分配,将 i赋值-1
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
          // 2)进行分配
            else if (U.compareAndSetInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
   //<5> i=-1: 已经没有任务分配 或 做最后的任务的线程已经完成元素转移
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
          //<5.1> 已经完成扩容 在<5.2>的时候进行判断是否是最后一个完成元素转移的线程(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT. 
          //与开始的时候呼应
            if (finishing) {
                nextTable = null;
                table = nextTab;
              //设置新的扩容阈值0.75*nextTable.length
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
          //<5.2> 将线程数-1,addCount方法中<1.1> 的 2)会使用到. 如果已经完成扩容finishing=true,并且i=n进行检查
            if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
              //与<7> 中的检查有关
                i = n; // recheck before commit
            }
        }
  //<6> 前置条件: 没有执行完任务. 使用CAS转移null节点,只需指向fwd即可. 
  //		true: 继续转移下一个slot false: 当前slot有数据了(因为在扩容时,允许向null的slot中写入数据 putVal方法的<1.1>),会自旋到<8> 转移数据.

        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
  //<7> 前置条件: 没有执行完任务. (fh = f.hash) == MOVED: 最后一个线程会对所有的slot进行检查,看是否有遗漏的slot,并将其完成转移.
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
  //<8> 锁住头节点,对非null slot进行元素转移. 
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                  // 1) 链表 使用头插法 
                  //		runBit: 对扩容后的table 1:高端 0: 低端 
                  //		lastRun: 最后的runBit相同的链首,因为new Node,所以节省了一定的空间
                    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;
                    }
                  // 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;
                            }
                        }
                        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;
                    }
                }
            }
        }
    }
}

为什么在进行转移链化的元素的时候需要 new Node(). 而在转移树化的元素的时候没有new TreeNode(),采用了和HashMap类似的方法?

我的理解: 避免在转移的过成中(因为在转移成功后才设置头节点为fwd节点, 所以在转移的过程中get还需使用oldTable中的数据而不是fwd节点指向的nextTable),get操作因为next 引用的改变导致查找丢失. 而 tree,利用的双向链表采用与HashMap一样的方法 是因为在查询的时候用的是treenode的左右节点 而没有使用prev和next, 所以对get操作不会影响.

所以HashMap如果一个线程在扩容的时候, 另一个线程进行get操作也是不安全的.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值