J.U.C之ConcurrentHashMap1.7/1.8全解析

jdk1.7/1.8 ConcurrentHashMap全解析

前言

      hashmap是一个性能十分优良的容器,但是在多线程情况下会产生线程安全问题
      在JDK1.7中由于采用头插法,在扩容时候会产生死循环造成CPU100%的使用率
      而在JDK1.8中虽然采用尾插法,但依旧会造成些许问题,如数据丢失等等问题,
      总之,在多线程环境下,如何保证数据的安全性是首要问题。
  • 采用Hashtable保证线程安全
    hashtable相当简易版的map,只采用链表解决hash冲突,默认初始化容
    量为11,key-value都不能为null,扩容是原来大小的2倍+1,这里主要是
    说明一下其如何保证线程安全性,如下所示:
    public synchronized V remove(Object key) {};
    public synchronized V put(K key, V value) {};
    public synchronized V get(Object key) {};
    public synchronized boolean containsKey(Object key) {};
    public synchronized void clear() {};
    public synchronized int size() {};
    所有关键的方法都使用synchronized保证同一时间内,只能有一个线程成
    功获取锁,在多个线程并发执行情况下,读,写操作竞争同一把锁,吞吐
    量大大降低。
  • 使用ConcurrentHashMap

    • 采用分段锁,每一个段Segment继承ReentrantLock进行同步,Segment内包含HashEntry数组,用来存储数据,扩容时候不用全局扩容,只在一个Segment内扩容,用volatile修饰value保证读不加锁。
    • Unsafe调用,屏蔽底层操作系统细节,以Api的形式直接访问或操作内存,性能好。

JDK1.7中具体实现

//Segment实现ReentrantLock
static final class Segment<K,V> extends ReentrantLock implements Serializable {	
         //key-value数组,也就是桶
        transient volatile HashEntry<K,V>[] table;
        transient int threshold;  //阈值
        final float loadFactor;   //负载因子
}

其数据结构就如下图说所示,相当于使用多把锁提升了并发度在这里插入图片描述

构造方法

public ConcurrentHashMap() {
    //空参构造,DEFAULT_INITIAL_CAPACITY = 16,DEFAULT_LOAD_FACTOR =0.75
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}

public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        // MAX_SEGMENTS 值为 65536
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // 这里用来求大于concurrencyLevel最小2 ^ n
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        // Segment[]的大小为2的 N 次方,segmentShift属性为32减去N,segmentMask属性为2的N次方减去1
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1; 
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // c 表示每个 Segment 中应该容纳多少个元素
        int c = initialCapacity / ssize;// 除后取整
        if (c * ssize < initialCapacity) 
            ++c;    
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        // 在这里创建数组 segments 和 segment[0]
        // 注意只初始化segment[0],其他的段延迟初始化
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        // 通过 Unsafe 调用, 将 s0 写入 segment[0]
        UNSAFE.putOrderedObject(ss, SBASE, s0); 
        this.segments = ss;
    }

put方法如下

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        //进行一次hash散列,定位在哪个segment上
        int j = (hash >>> segmentShift) & segmentMask;
        //Unsafe系统调用,如果没有初始化segment,先初始化
        //注意这里采用延时初始化,节省内存消耗
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            //确保初始化过了
            s = ensureSegment(j);
        // 把元素放置到对应的 segment[j] 中    
        return s.put(key, hash, value, false);
    }

注意这里有个ensureSegment方法

  1. 这里采用tryLock尝试获取锁,多个线程竞争情况下,当前线程若获取不到锁也不闲着,若对应的桶为空或者遍历到末尾根据Key找不到结点,就先初始化。
  2. 自旋到一定次数还是获取不到锁才进行阻塞
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {	
            HashEntry<K,V> first = entryForHash(this, hash); 
            // e用来迭代链表
            HashEntry<K,V> e = first;          
            HashEntry<K,V> node = null;
            int retries = -1; // 初始赋值为 -1,进入if (retries < 0) {}的标记
            while (!tryLock()) {// 自旋
            
                HashEntry<K,V> f; // 定义临时指针变量, 方便后续判断使用
                if (retries < 0) {
                // 锁竞争失败,也不闲着,看看有没有其他事可做
                    if (e == null) {
                        //  当前桶为空           
                        if (node == null) 
                            //初始化
                            node = new HashEntry<K,V>(hash, key, value, null);
                        //跳出该分支的标记
                        retries = 0;
                    }
                        // 根据key 找到node,说明node存在
                    else if (key.equals(e.key))
                       //跳出该分支的标记
                        retries = 0;
                    else
                       // 遍历链表
                        e = e.next;
                }
                else if (++retries > MAX_SCAN_RETRIES) {
                    //自旋到一定次数就直接阻塞
                    lock();
                    break;
                }
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                //并发情况下,有其他线程改变结构,所以每隔一次循环之后进行检测
                //检查桶中的头结点是否发生变化(可能其他线程进行删除或者添加操作)
                // retries重置为 -1,可以继续进行tryLock()尝试获取锁
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
            return node;
        }

总结一下:

  1. 通过key的hash值,先获取在segment数组中的下标(高位 & 偏移量)
  2. 调用Unsafe的getObject获取 segment
  3. 如果段没有初始化,进行初始化
  4. 尝试获取锁:
    1. 获取锁成功直接返回null的Node节点
      根据hash定位数组下标,遍历链表
      如果头节点为空或者遍历到末尾都没有找到key对应的Node,查看Node节点有没有创建
      有的话就设置到头,没有创建就创建Node,有判断是否要扩容,不用扩容就放入
    2. 当前线程获取锁失败
      标记为-1:(在遍历链表过程中,如果链表为空就创建一个Node节点,或者找到key对应的Node就退出标记)
      当尝试获取锁失败次数太多(cas)就阻塞
      由于尝试获取锁过程中可能有其他线程并发操作,每隔一次循环,就检查首结点,如果不同就将标

扩容操作
看过HashMap的扩容方法后,相信这里并没有什么困难,不过要注意的是这里有多个for循环,它们分别是做什么的呢?

  • 这里多个批量迁移判断,据Doug Lea说,正常情况下,扩容时候,每个桶平均有1 / 6的结点数目的位置发生变化,所以不需要一个一个结点的判断在高位还是低位链表,但是在特殊下,链表中结点位置扩容时重新计算刚好是交错着,这样就不容乐观了。
private void rehash(HashEntry<K,V> node) {
            HashEntry<K,V>[] oldTable = table;
            int oldCapacity = oldTable.length;
            //2倍于原先的桶数组大小
            int newCapacity = oldCapacity << 1;
            //新的阈值
            threshold = (int)(newCapacity * loadFactor);
            HashEntry<K,V>[] newTable =
                (HashEntry<K,V>[]) new HashEntry[newCapacity];
            //掩码,用来计算索引
            int sizeMask = newCapacity - 1;
            //从0号下标开始结点迁移操作
            for (int i = 0; i < oldCapacity ; i++) {
                HashEntry<K,V> e = oldTable[i];           
                if (e != null) {
                    HashEntry<K,V> next = e.next;
                    //重新计算hash
                    int idx = e.hash & sizeMask;
                    //原先桶中只有一个结点,直接放入新的桶数组
                    if (next == null)   
                        newTable[idx] = e;
                    else { // Reuse consecutive sequence at same slot
                        HashEntry<K,V> lastRun = e;
                        int lastIdx = idx;
                        //这里不同于hashmap中结点是一个一个进行迁移
                        //意思是说,每个结点都判断一下在高位还是在低
                        //位链表中,这里是一个批量迁移。
                        for (HashEntry<K,V> last = next;
                             last != null;
                             last = last.next) {
                            int k = last.hash & sizeMask;
                            if (k != lastIdx) {
                                lastIdx = k;
                                lastRun = last;
                            }
                        }
                        newTable[lastIdx] = lastRun;
                        //普通的迁移操作,还是一个一个结点进行判断
                        //这时候可能在高位,也可能在低位
                        for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                            V v = p.value;
                            int h = p.hash;
                            //重新计算hash
                            int k = h & sizeMask;
                            HashEntry<K,V> n = newTable[k];
                            newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                        }
                    }
                }
            }
            //最后将新添加的结点加入到新的桶数组中
            int nodeIndex = node.hash & sizeMask; // add the new node
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

JDK1.8中具体实现

1.8中实现和之前的大为不同,具体如下

  • 摒弃了Segment分段锁,采用Synchronized内置锁(说明1.8的Synchronized性能已经大大改进了,其增加了适应性自旋,可重入,偏向,轻量级锁等,这里先不详细说明)
  • 使用CAS方式,通过Unsafe调用直接操作内存,比较替换,相当于volatile语义,实际上也是通过lock前缀指令,根据缓存一致性原则,实现了轻量级的线程同步机制
  • 和Hashmap1.8中结构一直,采用数组,链表,红黑树数据结构,将查询时间复杂度降到O(logN)
  • 并发扩容,多个线程进行协助扩容
  • 并发计数,计算结点个数,并发情况下,多个线程参与计数,采用分段锁思想
  • 树结点加入了读写锁机制

下面介绍相关变量

    //最大桶数组大小 2 ^ 30,仅仅是为了序列化兼容,在1.8中不使用
    private static final int MAXIMUM_CAPACITY = 1 << 30;
    
    //默认桶数组大小 16,为了兼容性
    private static final int DEFAULT_CAPACITY = 16;
    
    //最大数组数限制
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
    //并发度,为了兼容性
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    //负载因子,同样是为了兼容
    private static final float LOAD_FACTOR = 0.75f;

    //链表转化为红黑树的条件之一
    static final int TREEIFY_THRESHOLD = 8;
  
    //树退化为链表的条件
    static final int UNTREEIFY_THRESHOLD = 6;

    //链表转化为红黑树的条件之一
    //只有当桶数组长度大于64,并且TREEIFY_THRESHOLD不小于8才转换
    static final int MIN_TREEIFY_CAPACITY = 64;

    //步长,扩容操作可以是多线程,扩容时候每个线程分配一个扩容任务,
    //也就是负责桶数组上一部分结点迁移操作,默认为16,也就是说,
    //一个线程负责16个桶的结点的操作,根据CPU数目计算所得,避免线程数
    //过多造成线程上下文切换频繁
    private static final int MIN_TRANSFER_STRIDE = 16;

    //用于生成扩容戳,用来作为扩容次数的标记,不同的扩容
    //次数,扩容戳不同,比如默认桶数组大小为16,第一次扩容
    //后为32,扩容戳为
    /* 具体计算方法:
       static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n)
                 | (1 << (RESIZE_STAMP_BITS - 1));
      }
      numberOfLeadingZeros这个方法根据传进来的数求出其二进制中第一
      不为0之前0的个数,听起来有些拗口
      比如32  0000 0000 0000 0000 0000 0000 0010 0000  从第一个bit为1往前有28个0
        
      比如64  0000 0000 0000 0000 0000 0000 0100 0000  从第一个bit为1往前有27个0
      假若开始容量是16 ,32是第一次扩容,64显而易见是第二次扩容,区别就是0的个数
      在和 1 << (RESIZE_STAMP_BITS - 1) 按位或,保证第16为1
     */
    private static int RESIZE_STAMP_BITS = 16;

    //扩容时候最大的线程数
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

     //偏移量,把生成戳移位后保存在sizeCtl中当做扩容线程计数的基数,反方向即可求出扩容戳
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

  
    static final int MOVED     = -1; //  表明在扩容阶段
    static final int TREEBIN   = -2; //  表明是树结点
    static final int RESERVED  = -3; // hash for transient reservations
    static final int HASH_BITS = 0x7fffffff; // 计算

    //核心CPU数目
    static final int NCPU = Runtime.getRuntime().availableProcessors();
 
    //桶数组
    transient volatile Node<K,V>[] table;

    //扩容时的临时桶数组
    private transient volatile Node<K,V>[] nextTable;

    //互斥变量,该变量有很多用途
    private transient volatile int sizeCtl;

    //下一个迁移任务在桶数组中的起始位置+1
    private transient volatile int transferIndex;
   
    //用来计数,统计桶数组结点个数
    private transient volatile long baseCount;
    
    //初始化cell数组的标志
    private transient volatile int cellsBusy;

    //cell数组,进行并发分段计数
    private transient volatile CounterCell[] counterCells;

这里重点介绍一下sizeCtl变量,以便之后的分析:

  • 首次初始化时(还没初始化), 其变量含义为初始容量
  • 正在初始化时,其值为 -1
  • 初始化结束,其值为阈值
  • 扩容时候,其为负值,高16位表示扩容戳,低16位表示协助扩容的线程个数
 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, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }
        // ......省略
      

get方法

  • 根据key计算hash
  • 边界值判断,判断桶数组是否初始化,并且使用CAS方式判断索引出有没有结点
  • 根据hash和key查找value
  • 可能是树结点或者是正在扩容阶段
  • 普通的链表结点,循环遍历
  public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        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;
    }

put方法

 final V putVal(K key, V value, boolean onlyIfAbsent) {
        //  key 和 value不能为空
        if (key == null || value == null) throw new NullPointerException();
        // 高16位和低16位按位异或计算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();
            //根据key的hash定位在桶数组中下标,Unsafe调用获取首结点
            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 
            }
            //如果是扩容阶段,该线程帮助扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            
            else {
                V oldVal = null;
                //获取当前首节点的对象锁
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                         //头节点hash值大于等于0,是正常的链表首节点
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //遍历链表找到key就退出
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                //链表中没有key对应的node创建node并退出
                                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;
                }
            }
        }
        //map中结点数目加一
        addCount(1L, binCount);
        return null;
    }


initTable方法用来初始化桶数组,注意这里使用延时初始化的方式,并且使用CAS方式保证只能有一个线程进行初始化,初始化时候进行直接将SIZECTL对应内存中的值变为-1,声明为初始化阶段,初始化完毕,SIZECTL表示阈值大小,十分巧妙

//延时初始化
private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        //自旋,保证至少有一个线程初始化
        while ((tab = table) == null || tab.length == 0) {
        //表示其他线程正在初始化或者扩容数组,当前线程释放CPU
        //yield()方法释放CPU的执行权到就绪状态
            if ((sc = sizeCtl) < 0)
                Thread.yield(); 
            //当前线程使用CAS操作将字段相对对象地址偏移量上的值改为-1
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try { //二次判断
                    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;
                        //初始化完成后,表示阈值(为原来0.75倍)
                        sc = n - (n >>> 2);  
                    }
                } finally {
                    sizeCtl = sc;   //初始化完成之后 sizeCtl成为阈值
                }
                break;
            }
        }
        return tab;
    }

并发计数,这里采用类似LongAdder计数工具类
这里采用Segment分段锁思想,多线程环境下使用hash算法初始化cell计数单元进行分段计数最后求和,和java.util.concurrent.atomic.LongAdder相似,避免了多线程的竞争,相对于AtomicInteger并发情况下只能有一个线程获取锁,其他线程自旋,性能更好

扩容

首先介绍几个重要的概念

  • int rs = resizeStamp(n);
    

    计算扩容戳,代表扩容的次数,查看是否是当前桶数组进行的扩容
    计算数组大小的二进制中第一个不为0的数之前0的个数和 1 << 15进行按位与操作,
    即16位是1,扩容时候,左移16位就成为负数表示扩容阶段

  • 注意sizeCtl = -(1 + nThreads)这里用来计算协助扩容数目,实际上并非这样计算,而是使用扩容戳左移16位生成一个负数加上n, n表示扩容的线程数。

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        //低并发情况下,cell数组不用初始化,CAS操作成功在原始值上计数
        //高并发情况下,cell数组若初始化直接进入if条件代码块,
        // cell数组没有初始化使用CAS方式修改baseCount失败(有竞争),进入代码块
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            //这里cell数组可能为空,直接进入if代码块
            //每个线程根据ThreadLocalRandom.getProbe()计算一个随机数,
              每个线程计算出的数值不同,之后根据hash算法计算索引,
              线程对应的cell没有初始化也进入if代码块
            //或者CAS失败进入if条件
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                //进行cell数组初始化等一系列操作
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            //并发计数
            s = sumCount();
        } //和longAdder思想相同,并发计数,线程进行hash运算定位cell,进行初始化技术,最后合并计算结果
        if (check >= 0) {
             
            Node<K,V>[] tab, nt; int n, sc;
            // 当计数大于阈值,桶数组不为空,进行扩容
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                // 计算扩容戳
                int rs = resizeStamp(n);
                if (sc < 0) {
                    //当前互斥变量sizeCtl 高16位不等于 rs 说明不是同一个扩容任务,或者没有扩容任务可进行分配直接退出
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == s + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                     //CAS方式将协助扩容的线程数目加一
                    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();
            }
        }
    }
//协助扩容,若是扩容阶段,当前线程协助扩容
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);
            //循环判断是扩容阶段
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                //sizeCtl无符号右移RESIZE_STAMP_SHIFT(偏移量默认16)可以反向求出
                //扩容戳,这里就比较是否右移之后的值是否等于扩容戳,意思就是说是不是
                //当前的扩容任务,再者判断是否还能分配扩容任务
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;   //不能分配扩容任务
                //当前进行任务分配,将SIZECTL加一,也就是协助扩容线程数加一
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                   //进行协助扩容
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }


private final void tryPresize(int size) {
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
        //桶数组进行初始化
        if (tab == null || (n = tab.length) == 0) {
            n = (sc > c) ? sc : c;
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if (table == tab) {
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                        sc = n - (n >>> 2); // 0.75 * n
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        //c <= sc表示数组已经初始化过, n >= MAXIMUM_CAPACITY表示桶数组到达最大值
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        else if (tab == table) {   //可以进行扩容
            //计算扩容戳,代表扩容的次数,查看是否是当前桶数组进行的扩容
            //计算数组大小的二进制中第一个不为0的数之前0的个数和 1 << 15进行按位与操作,
            //即16位是1,扩容时候,左移16位就成为负数表示扩容阶段
            int rs = resizeStamp(n);

            //表示正在扩容阶段
            if (sc < 0) {
                Node<K,V>[] nt;
                // 当前互斥变量无符号右移16(默认情况下是16)反向求出当前的扩容戳
                // 若不相同,则表示不是一个扩容任务,或者扩容任务为零,没有任务分配
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                //协助扩容,将sizeCtl加一,即协助线程数加一
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            //表示第一次扩容,使用cas操作将rs左移16位,即互斥变量是负数,
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                //新桶数组还没有初始化
                transfer(tab, null);
        }
    }
}
 //真正的结点迁移操作,其实并不是结点移到新的桶数组上,而是采用复制操作
 //将结点复制到新的数组上,这样可以保证get时候获取到数据,不过是一种快照读
 //综上说明concurrenthashmap是弱一致性的,并不能保证实时数据
  private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        //求取任务步长,第一个任务从数组下标最大开始往前移动一定数目
        //4核cpu情况下桶数组长度不大于512,默认步数为16,8核cpu情况下桶数组长度
        //不大于1024,默认步数为16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        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;
            }
            //concurrenthashmap中成员变量,表示新桶数组
            nextTable = nextTab;
            //任务索引,开始时候指向桶数组末尾下标,来一个线程分配一定步长的桶
            transferIndex = n;
        }
        int nextn = nextTab.length;
        //当迁移线程完成“一个桶”的全部元素的迁移后, 旧数组中该桶所在的位置会被赋成一个       ForwardingNode,当有线程需要访问时候转发到新桶数组上
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
       
        boolean advance = true;    //表示一个迁移任务完成,可以开始下一个任务

        boolean finishing = false;  // 扩容任务结束将此值设为true
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                //表示当前一次扩容任务还未完成,还在当前任务的范围内
                if (--i >= bound || finishing)
                    advance = false;
                //从桶数组下标自大而小即从末尾往前遍历,每一个步长分配一个任务,到达头位置结束
                //任务分配完毕,当前线程没有迁移任务,可以退出扩容
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;                     
                    advance = false;     //退出循环
                }
                //任务可分配,使用CAS方式将TRANSFERINDEX减小步长数
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    // 本次迁移任务开始下标
                    bound = nextBound;
                    // 本次迁移任务的末尾下标
                    i = nextIndex - 1;
                    advance = false;
                }
            }
     //i < 0 :表示本次transfer任务已经执行完毕了
     //i >= n :不是同一次扩容任务,任务作废,由最后一个线程检查
     //i + n >= nextn :这个条件我不知道怎样理解
            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置为true退出循环
                    finishing = advance = true;
                    i = n;    //这一部分我没有看懂
                }
            } 
            
            else if ((f = tabAt(tab, i)) == null)
                //volitail读为空,直接在原来桶数组的对应索引放入转发节点
                advance = casTabAt(tab, i, null, fwd);
                //已经扩容过了,需要进行检查
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            //位标志 节点hash按位与 旧数组大小,求得节点在原来下标出还是,原来下标处+旧数                              //组大小
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            //和jdk1.7concurrenthashmap中一致,扩容不是一个一个节点的迁移,而是批量迁移
                            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);
                            }
                            //CAS将高低位链表加入新的桶数组中
                            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;
                        }
                    }
                }
            }
        }
    }

总结

如此看来,ConcurrentHashMap真是不容易分析,而且关于Stream部分还未来得及分析,目前先介绍几个重要的方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值