java 8 ConcurrentHashMap 源码详解

175 阅读11分钟

数据结构

1.81.7不一样了,1.7segment+HashEntry组成,1.8是由数组+链表+红黑树,倒是跟1.8HashMap有点像

问题

  • 1.7是通过segment分段锁实现线程安全的,1.8没有了segment是怎么保证线程安全的

数组大小初始化

    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
         // initialCapacity + (initialCapacity >>> 1) + 1
         //这个传入就会比 initialCapacity 大很多,不止二的幂次方了
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }
   //下面这个方法 跟HashMap 1.8一样
    private static final int tableSizeFor(int c) {
        int n = c - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

initTable

数组的初始化

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        //当数组为空时候进行死循环
        while ((tab = table) == null || tab.length == 0) {
        //1.sizeCtl没有初始化,所以为零
        //2.sizeCtl用volatile修饰了,保障了可见性
        //3.sizeCtl 是什么?相当于阈值
            if ((sc = sizeCtl) < 0)
            //如果 sizeCtl 小于零说明现在有线程在执行初始化数组
            //就展示放弃该线程cpu执行权
                Thread.yield(); // lost initialization race; just spin
            //如果sizeCtl不小于零,通过cas修改 sizeCtl 的值 为 -1,表示
            //现在有线程在执行数组初始化
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                //cas成功之后再次判断 数组是否为空,为空才进行初始化
                    if ((tab = table) == null || tab.length == 0) {
                    //默认是16
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                    //初始化数组容量    
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                    // n - (n >>> 2) 可以看作n减去(n除以四分之一)等于四分之三n
                    //不就是等于1.7的加载因子0.75
                    //sc 等于0.75n,也就是阈值
                        sc = n - (n >>> 2);
                    }
                } finally {
                //sizeCtl 就是阈值
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

initTable 总结:

  • 1.initTable怎么保证线程安全的?通过sizeCtl来控制的
  • 2.当有线程过来会先判断sizeCtl是否小于零,如果小于零就放弃cpu执行权
  • 3.如果sizeCtl不是小于零,也就是初始化0,就用cassizeCtl修改为-1
  • 4.cas修改成功之后,会进行数组的初始化
  • 5.1.8的阈值的计算跟之前不一样,之前直接是数组大小*0.751.8是通过n-(n >>> 2)来计算的,可能作者觉得这样效率更高还是咋地了
  • 6.所以数组初始化是通过cas控制sizeCtl来保证线程安全的

putVal

  final V putVal(K key, V value, boolean onlyIfAbsent) {
  //跟1.7一样,key不能为空
        if (key == null || value == null) throw new NullPointerException();
 //1.8的取hash比较简单,就是取原来的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();
            //定位出数组位置,如果数组位置为空,通过`U.getObjectVolatile`来获取数组位置上值
            //就通过`U.getObjectVolatile`来设置数组大小
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //U.compareAndSwapObject cas设置数组
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //如果头节点的 hash=-1,表示现在节点正在扩容,会通过 helpTransfer 帮助扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //对头节点 f 加锁
                synchronized (f) {
                //tabAt 获取内存可见的数组,再次判断头节点是否等于 f
                    if (tabAt(tab, i) == f) {
                    //fh >= 0 表示没有在进行扩容
                        if (fh >= 0) {
                            binCount = 1;
                        //遍历整个链表,binCount 记录遍历的次数
                        //binCount = 原链表长度
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //如果有key相同的,直接替换value,然后跳出循环
                                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) {
                //转链表的条件 binCount >= 8 也就是说链表原有八个,新增一个
                //此时就可以转红黑树,这个跟1.8的 HashMap 一样只是 binCount 的获取方式不一样
                    if (binCount >= TREEIFY_THRESHOLD)
                    //树化
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //增加容器长度
        addCount(1L, binCount);
        return null;
    }

putVal 总结:

  • 1.putVal会先去判断数据是否未初始化,如果没有初始化就初始化容器initTable
  • 2.如果定位到的数组的头节点为空,就通过cas设置数组位置上的头节点
  • 3.如果数组头节点不为空,会通过头节点的hash是否等于-1来判断是否在扩容,如果在扩容就调用helpTransfer来帮助扩容
  • 4.以上都不满足就会取正在的添加节点,为了保证添加节点的过程线程安全,会对头节点这个对象进行加锁
  • 5.加锁之后有两个分支,一个是链表,一个是红黑树
  • 6.如果是链表就遍历链表,先判断是否有key相同,如果相同就覆盖value,如果不同就尾插,同时记录下原链表长度
  • 7.如果是红黑树就走 putTreeVal去添加节点
  • 8.最后会通过链表长度来判断是否需要链表转化为树,条件是链表的长度大于等于8,这个跟1.8HashMap一样
  • 9.以上都执行完如果有新增节点,就会调用addCount去往容器大小加一
  • 10.需要注意点是在往数组添加头节点是通过cas来保证线程安全,在往链表和红黑树添加节点的时候是通过在头节点上加synchronized锁,有别于1.7ConcurrentHashMap通过ReentrantLock来锁住segment,用的锁和锁的对象都不一样。
  • 11.这个方法还差帮助扩容:helpTransfer,插入树节点:putTreeVal,树化:treeifyBin,统计长度:addCount

putTreeVal 插入树节点

需要注意的是1.8ConcurrentHashMap1.8HashMap不一样的是:ConcurrentHaShMap 增加了TreeBinTreeBin会记录树的头节点,然后数组存放的元素也是TreeBin为什么要加TreeBin这个类,因为我们加锁一般是对头节点进行加锁,每次往树加入节点都有可能进行树平衡,导致树的头节点变化,这个加锁的对象可能就错了,而引入TreeBin这个对象放到数组中,每次从数据获取到的对象都是他,不会因为树的平衡而导致树头节点变化。

//继承了 Node 
static final class TreeBin<K,V> extends Node<K,V> {
//记录着树头节点
        TreeNode<K,V> root;
//链表的头节点      
        volatile TreeNode<K,V> first;
        volatile Thread waiter;
        volatile int lockState;
        // values for lockState
        static final int WRITER = 1; // set while holding write lock
        static final int WAITER = 2; // set when waiting for write lock
        static final int READER = 4;
}
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
            for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
                //如果头节点是空,新增的节点就是头节点也是链表头节点
                if (p == null) {
                    first = root = new TreeNode<K,V>(h, k, v, null, null);
                    break;
                }
                //跟1.8的hashmap一样,通过各种比较判断新增节点是左节点还是右节点
                else if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                    return p;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        if (((ch = p.left) != null &&
                             (q = ch.findTreeNode(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.findTreeNode(h, k, kc)) != null))
                            return q;
                    }
                    dir = tieBreakOrder(k, pk);
                }
             //找到要插入的位置,开始插入新节点
                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                //设置链表的头节点为新增的节点
                //红黑树的链表采用的是头插法
                    TreeNode<K,V> x, f = first;
                    first = x = new TreeNode<K,V>(h, k, v, f, xp);
                    if (f != null)
                    //双向链表
                        f.prev = x;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    //如果父节点是黑色,就直接将新增节点置为红色,不需要去平衡树
                    //因为此时是满足红黑树的各种条件
                    if (!xp.red)
                        x.red = true;
                    else {
                    //父节点是红色,需要进行树的平衡
                    //lockRoot 去通过cas将 lockState 
                    //锁标志从0(表示没有线程在进行平衡树)改为1(表示有线程在平衡树)
                    //如果cas成功就进行平衡树,如果cas失败就park线程
                        lockRoot();
                        try {
                        //跟1.8的hashmap一样,不看
                            root = balanceInsertion(root, x);
                        } finally {
                        //直接 lockState = 0 去释放锁
                            unlockRoot();
                        }
                    }
                    break;
                }
            }
            assert checkInvariants(root);
            return null;
        }

putTreeVal 总结:

  • 1.插入树的逻辑跟1.8HashMap大体类似,不一样的有两点
  • 2.第一点增加了TreeBin来保证加锁过程中节点的不变
  • 3.第二点是在平衡树的时候balanceInsertion,会通过cas修改lockState值来加锁,保证balanceInsertion平衡树的线程安全

treeifyBin 树化

  private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
        //准备树化的时候,如果数组长度小于64就进行扩容
        //这点跟1.8的HashMap一样
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            //对头节点进行加锁
                synchronized (b) {
                //重复判断下,头节点有没有变
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        //遍历单项链表,把单项链表改成树,同时形成双向链表
                        //这个也跟1.8的Hashmap一样
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        //1.new一个 TreeBin,其实就是去设置树,平衡树然后把头节点放到
                        //TreeBin 的root属性上,new TreeBin 里面的树化跟1.8的Hashmap一样,不看了
                        //2.setTabAt,是把 TreeBin 放到数组的位置上
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

treeifyBin 总结

  • 1.树化过程还是基本跟1.8HashMap一样,不同有两点
  • 2.第一点:会对头节点synchronized加锁
  • 3.第二点:数组位置放的不是树的头节点,而是TreeBin对象,TreeBin对象有头节点属性
  • 4.TreeBin的创建过程跟1.8HashMap树化一样

tryPresize 扩容

todo transfer

helpTransfer 帮助扩容

todo

addCount 统计大小

  • ConcurrentHashMap大小是通过两个属性控制的
  • 一个是volatile long baseCount
  • 一个是volatile CounterCell[] counterCells数组,数组里面的元素CounterCell维护着volatile long value
 private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        //会先去判断 counterCells 数组是否空,如果为空,就通过 cas 往 baseCount 加一
        //counterCells 为空 或者 (counterCells不为空而且修改baseCount失败)就走下面的逻辑
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            //有几个情况会走 fullAddCount 去添加容器大小
            //1.counterCells 空
            //2.通过 & 来定位的 counterCells 为空
            //3.通过cas 往 counterCells 定位上的位置 value+1 失败
            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;
            //完成容器+1 之后进行统计    
            s = sumCount();
        }
        //check 其实就是 putVal 的bincount
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            //如果 容器大小 s 大于等于阈值 sizeCtl 就进行扩容
            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();
            }
        }
    }

fullAddCount

往容器大小加 x,主要是要往baseCount或者counterCellsx

private final void fullAddCount(long x, boolean wasUncontended) {
       //h 是通过 ThreadLocalRandom 为每个线程生成一个随机数
       //每个线程都不一样,是用来定位每个线程要操作 `counterCells` 
       //其实就相当于 hash 用来定位数组的位置一样
        int h;
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            //最外层的if,如果 counterCells 不为空就走下面的逻辑
            if ((as = counterCells) != null && (n = as.length) > 0) {
            //下面都是 counterCells 数组不为空的情况
            //如果定位到 counterCells 某个坑位 为空 就去初始化
                if ((a = as[(n - 1) & h]) == null) {
                //判断 cellsBusy 是否等于0 等于0表示现在没有线程在操作 counterCells
                    if (cellsBusy == 0) {            // Try to attach new Cell
                    //初始化一个 CounterCell 对象
                        CounterCell r = new CounterCell(x); // Optimistic create
                        //再去判断 cellsBusy 是否等于0 
                        //然后通过cas 去修改 cellsBusy 的值为1 表示现在有线程在修改 counterCells
                        if (cellsBusy == 0 &&
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            //如果cas 成功
                            boolean created = false;
                            try {               // Recheck under lock
                            //将 CounterCell 元素放到 CounterCells数组里面
                                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;
                }
                //如果上面的if失败也就是说定位的坑有已经有值了,就会走下面这些if
                //wasUncontended 如果外面有一次cas 去修改 counterCells 失败了
                //wasUncontended 就等于false 否则就是true
                //如果wasUncontended = false 就修改 wasUncontended = true 然后再次循环
                 else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                //如果 wasUncontended=true 就会通过cas 去修改 数组坑位上的值,修改成功就跳出
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                //如果 counterCells 发生变化 或者 counterCells 长度大于等于数组的核心树
                //collide = false 表示不进行 counterCells 扩容了
                else if (counterCells != as || n >= NCPU)
                    collide = false;            // At max size or stale
                //如果 collide = false 就会在这个if里面把 collide 改为 true
                //这个一直都走不到下面扩容的逻辑因为上面的if改为false,下面这个
                //如果 collide =false就会走这个逻辑
                else if (!collide)
                    collide = true;
                //如果可以扩容也就是 collide=true
                //先判断 cellsBusy 是否等于0 也就是说没有线程在操作 counterCells
                //通过cas 去修改 cellsBusy = 1 表示现在有线程在修改 counterCells
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                    //开始对 counterCells 进行扩容 大小是原来的两倍
                    
                        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
                }
                //扩容之后,重新通过 ThreadLocalRandom 去获取h相当于重新hash一下
                h = ThreadLocalRandom.advanceProbe(h);
            }
            //如果 counterCells 为空,就走这个最外层的if
            //通过cas 将 CELLSBUSY 修改为 1 表示目前有线程正在修改 counterCells 
            //如果cas CELLSBUSY 成功 就说明没有线程在修改 counterCells,走下面的逻辑
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                boolean init = false;
                try {                           // Initialize table
                //初始化 counterCells 数组 默认大小2
                //h & 1 定位到需要修改的位置
                //设置 counterCells
                    if (counterCells == as) {
                        CounterCell[] rs = new CounterCell[2];
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                //最后把 cellsBusy 修改为 0 表示这个线程已经操作完 counterCells 
                //其他线程可以接着操作
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            //如果上面的两个if 都不成功,可能走到第二个if,有其他线程已经初始化了 counterCells
            //就走这个 最外层的if 通过cas 修改 baseCount
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

fullAddCount 总结:

  • 1.为了保证往 容器大小加一线程安全 ConcurrentHashMap 维护了CounterCells数组和baseCount来保证线程安全
  • 2.baseCount比较简单就是通过cas来修改
  • 3.CountCells比较复杂,要保证这个数组操作的线程安全,主要通过cellsBusycas来保证对counterCells操作的线程安全
  • 4.每次需要操作counterCells都会cas cellsBusy,如果cas成功,就会到定位的counterCells位置上去修改valuecas失败会去各种尝试,还会去对counterCells进行扩容,扩容大小是原来数组大两倍,扩容之后会去重新通过ThreadLocalRandom获取h,相当于重新hash为了就是获得新的坑位。

sumCount

统计容器大小

final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        //baseCount 加上遍历 counterCells 数组
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

其实就是 baseCount+counterCells上每个坑位的value

未完待续.................