ConcurrentHashMap学习

map线程安全的集合

呃,我们比较常用的HashMap和LinkedHashMap都不是不是线程安全的, HashTable和Properties是线程安全的, 那为什么还要ConcurrentHashMap呢?
关键点就是在于找到hash表最小冲突的地方进行加锁,这也才能提高效率, HashTable和Preperties都是直接在方法上加锁,那么锁的粒度就是整个集合对象。这样合理吗? 想一下hash映射, 假设来20个线程一起并发, 刚刚好20个线程hash映射之后没有发生hash冲突,都到了不同的hash槽位上,你说这需要锁码?所以我们可以找到更小的粒度就是hash槽位, 因为只有线程之间产生hash冲突到同一个槽位的时候才会出现并发问题,所以我们需要对槽位进行加锁。
口嗨一下罢了, 里面还有很多牛逼的细节, 真正内部逻辑复杂的很,不仅需要hashMap的前置只是,还需要JUC的知识真的难。

节点类型

ConcurrentHashMap作为一个并发集合,多线程之间可见性问题必须保证,所以volatile 或者 cas 以及 unsafe.getObjectVolatile(),synchronized 都是保证可见性的基石
在这里插入图片描述
绝了,和HashMap相比,多了2到3个。。。

构造方法

构造方法有两个,有些问题,一个使用加载因子,一个使用位移,两个获取到的结果有些不一样

public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)  /*初始容量不合法*/
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY : //如果初始容量大于等于最大容量的一半,使用最大容量
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); // 容量调整
        this.sizeCtl = cap; //初始化容量--table表的大小
    }

sizeCtl这个时候代表的意思就是 table待会初始化时候的容量大小

initTable 初始化table表

多个线程put的时候发现如果table没有初始化,都会指向这个方法,所以这个方法是多线程执行的,所以需要保证线程安全性。 设计思路:cas竞争成功的去初始化,失败的就while循环判断直到table初始化完成,一直循环导致浪费cpu,所以可以使用Thread.yeild来(尝试)释放cpu的使用权。。
这里注意sizeCtl这个时候的意思,sizeCtl一开始被赋值了table初始化的容量,所以cas竞争的初始化标志就是那个线程吧sizeCtl修改成了-1,那个这个吸线程就去出初始化table表,别的线程就礼让cpu使用权

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {// 10个线程来,只有一个线程抢到了进行初始化,其他的线程等等着用whiel循环,只有table初始化完才能出去这个方法
            if ((sc = sizeCtl) < 0) //如果sizeCtl小于0, 说明其它线程正在扩容,当前线程让出CPU执行权
                Thread.yield(); // lost initialization race; just spin 禁止失败,自旋
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //多线程cas 竞争初始化table的资格
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY; //构造函数是否传入初始化容量,没有就使用默认的容量16
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; //初始化
                        table = tab = nt; //volatile的写
                        sc = n - (n >>> 2); //(3/4)*n = 0.75n  阈值 下一次table扩容的阈值,
                    }
                } finally {
                    sizeCtl = sc; //sizeCtl的含义从 初始化table容量变成了下一次扩容的阈值, 这里也就释放了cas锁
                }
                break;
            }
        }
        return tab;
    }

spread 扰动函数

同样是为了是的散列均匀

 static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS; //高位和地位进行异或,HASH_BITS 是为了避免为负
    }

putVal

思考:这个方法需要考虑那些问题?
1、哈希槽位的竞争 2、冲突之后的同步 3、table表初始化的时候进行等待

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode()); //避免hash碰撞的扰动函数, 获取hash
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {//table一开始是null的,第一次put的时候进行初始化, table是volatiles, 每次循环读都能读到最新的, 为啥使用for循环,因为多线程竞争,失败了的要进行重试
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0) //table表没有进行初始化
                tab = initTable(); //初始化hash表,这个方法是多线程执行的,需要保证安全性
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //(n - 1) & hash 确定槽位
                if (casTabAt(tab, i, null, //cas 竞争
                             new Node<K,V>(hash, key, value, null))) //cas槽位竞争 这里cas底层用lock指令前缀保证了可见性, 虽然数组里面的本身是没有可见性的
                    break;                    // no lock when adding to empty bin 竞争成功put成功了 直接break
            }
            else if ((fh = f.hash) == MOVED) //这个槽位的节点已经发生扩容 迁移状态
                tab = helpTransfer(tab, f); //当前线程协助去迁移。(我的槽位迁移了,帮助槽位中的元素都全部迁移完成之后,我在进行put)
            else { //发生冲突。槽位的节点不是迁移状态,可以安全进行地址链表法解决冲突(链表和红黑树)
                V oldVal = null;
                synchronized (f) { //对槽位进行加锁
                    if (tabAt(tab, i) == f) { //判断f指向的node节点是否还在槽位, 为啥需要判断 f是否还在槽位中, 比如当前线程在获取了f到对f加锁这个时间中,另外一个线程已经把f进行移除,但是当前线程还保留这个引用,所以需要在判断一下。
                        if (fh >= 0) { //正常节点 链表, 节点类型为Node 
                            binCount = 1; //计数,循环从槽位第一个节点开始, 所以初始值为1
                            for (Node<K,V> e = f;; ++binCount) { //替换或者塞入末尾
                                K ek;
                                if (e.hash == hash &&  //hash相等
                                    ((ek = e.key) == key || //key相等
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent) //onlyIfAbsent 仅当没有值的时候才赋值
                                        e.val = value; //替换
                                    break;
                                }
                                Node<K,V> pred = e; //pred 记住前驱 
                                if ((e = e.next) == null) { //如果为null 说明已经到达了链表末尾,挂上去
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;//binCount 不会计数新增的节点 binCount为当前新接入的node节点前面一共有多少个节点(包括了槽位)
                                }
                            }
                        }
                        else if (f instanceof TreeBin) { //说明槽位上的节点是红黑树的代理标识。即已经进行了树化
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) { //寻找key相等的红黑树节点
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) { //
                    if (binCount >= TREEIFY_THRESHOLD)  //是否达到扩容阈值,达到扩容阈值,当前链表上一共有9个
                        treeifyBin(tab, i); //树化逻辑
                    if (oldVal != null)
                        return oldVal; //说明是替换逻辑,baseCount不需要加一
                    break;
                }
            }
        }
        addCount(1L, binCount); //计数扩容
        return null;
    }

每个判断条件失败都要去重新进行volatile table的读,保证了可见性。。。 思考个问题 加锁的同步代码块,没有包裹树化逻辑和addCount ,会不会有线程安全问题?

tabAt 获取元素

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); //这里为啥要用volatile因为数组里面的值需要可见性。组数加了volatile只是保证数组变量的可见性并不保证,数组元素里面的值可见性
    }

treeifyBin 树化

private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY) //如果数组容量小于64,还是优先扩容,本次不进行树化
                tryPresize(n << 1); //扩容一倍, table的容量
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {//tabAt能获取最新的槽位的值, hash>0 说明是正常的节点
                synchronized (b) { //对槽位进行加锁
                    if (tabAt(tab, index) == b) { //判断是否被移除了
                        TreeNode<K,V> hd = null, tl = null; //tl为尾指针 hd为头指针
                        for (Node<K,V> e = b; e != null; e = e.next) { //把Node类型的单向链表,变成TreeNode的双向链表 
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);  //节点类型转化成TreeNode
                            if ((p.prev = tl) == null) //双向链表 尾插法
                                hd = p;
                            else
                                tl.next = p;
                            tl = p; //指向尾巴
                        }
                        setTabAt(tab, index, new TreeBin<K,V>(hd)); //将双向链表转化为红黑树, 在TreeBin的构造方法里面转化为红黑树 
                    }
                }
            }
        }
    }

不出意料为了当前线程扩容时候的安全, 对该hash槽位进行了加锁。在变成TreeNode的双向链表之后, 使用了TreeBin的构造方法生成红黑树。 TreeBin里面的成员属性有TreeNode类型的,正是用来指向红黑树的根节点, 然后把TreeBin对象 放入hash槽。

TreeBin

TreeBin(TreeNode<K,V> b) {
            super(TREEBIN, null, null, null); //设置为-2,代表TreeBin是一个红黑树代理节点, 这里也就说明了如果hash值为-2,那么这个槽位就是TreeBin 
            this.first = b; //TreeBin不仅使用root记录了红黑树的根节点,还使用first记录了双向链表的头指针
            TreeNode<K,V> r = null; //r 根节点
            for (TreeNode<K,V> x = b, next; x != null; x = next){ //从双向链表里面拿出一个节点
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (r == null) {
                    x.parent  = null; //根节点的父亲节点为null
                    x.red = false; // 红黑树的根节点为 黑色。
                    r = x; //r 记录根节点
                }
                else { //红黑树的put方法
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = r;;) { //这里就是从根节点开始比较找到叶子节点
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h) //往左子树走
                            dir = -1;
                        else if (ph < h) //往右子树走
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) || //没有比较器
                                 (dir = compareComparables(kc, k, pk)) == 0) //比较结果为相等
                            dir = tieBreakOrder(k, pk); //继续比较
                            TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) { //p开始移动
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            r = balanceInsertion(r, x); //插入之后要进行平衡
                            break;
                        }
                    }
                }
            }
            this.root = r; //红黑树的根节点
            assert checkInvariants(root); //检查红黑树的性质
        }

插入之后保持黑色平衡就不看了,和hashMap的一样。。。

putTreeVal

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;
                }
                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(); //加寄生读写锁(寄生在Synchronized上), TreeBin里面不用加锁是因为,TreeBin是调整之后才给Root赋值
                        try {
                            root = balanceInsertion(root, x);
                        } finally {
                            unlockRoot(); //
                        }
                    }
                    break;
                }
            }
            assert checkInvariants(root); //检查是否符合红黑树性质
            return null;
        }

这部分代码和 HashMap的差不多, 但是 思考一下,调用putTreeVal外围不是已经对槽位进行加锁了,那为啥还要进行 黑色平衡的前后加上lockRoo和unLockRoot,原因就获取读的时候与写不进行互斥,原因就是在于调整的时候会对红黑树的parent和left right进行调整,如果这时候读还是走红黑树的查询,那坑定会有问题,但是别忘记了TreeBin 的first 还维护了一个TreeNode的双向链表结构,所以有写线程的时候,依旧可以读,只不过说读效率没有走红黑树那么块

TreeBin结构

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; // increment value for setting read lock 读锁
}

TreeBin的lockState会表示当前槽位的状态,put的时候首先会在槽位加synchronized锁,所以写线程只能有一个, 但是读的话 是不用在槽位加synchronized的, 读的时候判断TreeBin的lockState如果没有写锁或则和等待写锁的 那么直接cas修改lockState为 读锁, 同样黑色平衡调整的时候判断lockState的状态是不是 读锁,如果读锁那么久进行park, 不是那就尝试把lockState修改成读锁

lockRoot

private final void lockRoot() {
            if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER)) //获取写锁失败 这里是和读线程竞争
                contendedLock(); // offload to separate method
        }

contendedLock

private final void contendedLock() { //lockState 的状态 0001 是写锁 0010 是等待获取写锁  0100 有别的线程持有读锁
            boolean waiting = false; // 本方法是写锁线程是串行执行(synchronized 槽位) lockState的组合类型有 0001, 0110, 0100,0010
            for (int s;;) {
                if (((s = lockState) & ~WAITER) == 0) {//没有读锁或者写锁 0010 
                    if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) { //拿到写锁 0001
                        if (waiting)
                            waiter = null; //当前线程已经获取到写锁 ,清除等待标记
                        return;  
                    }
                }
                else if ((s & WAITER) == 0) { //说明读锁被占了, 我需给lockState加上 等待标志
                    if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) { // 0110
                        waiting = true;
                        waiter = Thread.currentThread();
                    }
                }
                else if (waiting) // 别人持有读锁了,写线程挂起, 读线程会把自己唤醒
                    LockSupport.park(this);
            }
        }

unlockRoot

private final void unlockRoot() {
            lockState = 0;
        }

get

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode()); //获取hash
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) { //hash槽位上有值
            if ((eh = e.hash) == h) { //直接进行判断
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0) //不是正常节点 (TreeBin 红黑树上去查找 或者 ForwardingNode 转发到nextTable上去查找 或者 reservationNode 占位符 直接放回null)
                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;
    }

find (TreeBin)

在这里插入图片描述

final Node<K,V> find(int h, Object k) { //多线程执行
            if (k != null) {
                for (Node<K,V> e = first; e != null; ) { 
                    int s; K ek;
                    if (((s = lockState) & (WAITER|WRITER)) != 0) { //这里是判断有没有写线程持有写锁,或者准备等待获取写锁, 那么我就不适用红黑树继续查找,而是用双向链表进行查找 
                        if (e.hash == h &&                           //写线程在等待的时候可能进入park, 这里发现是等待状态,就不去走红黑树查询
                            ((ek = e.key) == k || (ek != null && k.equals(ek))))
                            return e;
                        e = e.next;
                    }
                    else if (U.compareAndSwapInt(this, LOCKSTATE, s,
                                                 s + READER)) { //加读锁 每个读锁都会加4
                        TreeNode<K,V> r, p;
                        try {
                            p = ((r = root) == null ? null :
                                 r.findTreeNode(h, k, null)); //根节点开始找
                        } finally {
                            Thread w;
                            if (U.getAndAddInt(this, LOCKSTATE, -READER) == //-4 也就是减少一个读锁
                                (READER|WAITER) && (w = waiter) != null) //之前的状态是一个读锁加写锁等待
                                LockSupport.unpark(w); //释放了所有的读锁,那就去唤醒等待的线程
                        }
                        return p;
                    }
                }
            }
            return null;
        }

addCount 计数+扩容

累加计数这块设计和LongAddr一样,借助多核的优势,通过空间换时间,提高效率。

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        if ((as = counterCells) != null || //计数器数组不为null
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { //直接在baseCount上加,cas失败,表示竞争激烈 
            CounterCell a; long v; int m;
            boolean uncontended = true; //没有竞争
            if (as == null || (m = as.length - 1) < 0 || //如果没有初始化,或者初始化了长度为0
                (a = as[ThreadLocalRandom.getProbe() & m]) == null || //计数器槽位没有值
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { //在该计数器槽位进行cas竞争失败
                fullAddCount(x, uncontended); //竞争失败 对于LongAdderd 的 longAccumulate
                return; //竞争失败的就不参与扩容了,让竞争成功的去扩容
            }
            if (check <= 1) //?? 槽位上数量太少??
                return;
            s = sumCount(); //获取总的计数
        } //第一次直接竞争成功的都会来进行扩容
        if (check >= 0) { // 整个方法并没有加锁 
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) { //达到阈值并且数组不为空并且table的长度小于最大容量
                int rs = resizeStamp(n);  //根据原table的容量获取一个扩容的邮戳? 容量n转化为二进制后左边0的个数加上2^15
                if (sc < 0) { //说明此时正在扩容  (sc >>> RESIZE_STAMP_SHIFT) != rs 表示有人已经扩容了 即table的长度已经发生改变
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||  //sc==rs+1 这里是BUG jdk8的 正确写法 sc==rs<<RESIZE_STAMP_SHIFT + 1 , 说明当前只有最后一个线程了(第一个线程设置的线程数量为2,别的参与扩容都是加1),迁移已经完成了
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null || //sc == rs + MAX_RESIZERS => sc==rs<<RESIZE_STAMP_SHIFT+MAX_RESIZERS 可以帮吗扩容的线程数量已经达到最大了  nextTable为null 扩容也结束了
                        transferIndex <= 0) //迁移的时候 hash槽下标从末尾开始, transferIndex <=0 说明所有的hash槽位已经都被别人分配去扩容了
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))  //参与扩容
                        transfer(tab, nt);
                } //此时还没有扩容
                else if (U.compareAndSwapInt(this, SIZECTL, sc,  //rs 低16位为1, 所以左移16位之后一定是负数
                                             (rs << RESIZE_STAMP_SHIFT) + 2)) //sc>0 , 说明当前table还没有扩容, 这个时候sizeCtl含义 高16位为邮戳,低16为线程数量, 但是为啥加2??
                    transfer(tab, null); //开始扩容, 迁移
                s = sumCount(); //获取总的计数
            }
        }
    }

从代码中可以看出不是所有的线程都会参与扩容
sizeCtl从容量的含义变成了邮戳号(高16位)+扩容线程数了(低16位)

transfer

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride; //stride 线程处理的hash槽位数
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) //NCPU cpu的核心数
            stride = MIN_TRANSFER_STRIDE; // subdivide range 最小的处理hash槽位数量为16
        if (nextTab == null) {            // initiating 初始化
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; //扩容容量为原来的2倍
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab; //volatile的写 
            transferIndex = n; //从原table末尾开始迁移   
        }
        int nextn = nextTab.length; //新table长度
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); //这里的ForwardingNode里面持有nextTable, 槽位赋值整个类型的,hash值为-1,代表正在扩容迁移, 每个线程都会创建一个
        boolean advance = true; //该标识代表要不要处理下一个hash桶
        boolean finishing = false; // to ensure sweep before committing nextTab 控制扩容何时结束
        for (int i = 0, bound = 0;;) { //开始处理hash桶 [开头, 结尾]
            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;
                }
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?  //剩余的槽位还比strde(最小分配槽位)小的话
                                       nextIndex - stride : 0))) { //cas竞争 分配任务
                    bound = nextBound; 
                    i = nextIndex - 1; //往前推荐槽位
                    advance = false;
                }
            }//任务分配完成,开始迁移
            if (i < 0 || i >= n || i + n >= nextn) { //i<0 说明 当前线程不需要帮助迁移槽位了,任务完成了 i>=n 和 i+n >= nextn 都是技术上的判断一个是原数组不会越界,另外一个是扩容二倍一定大于i+n
                int sc;
                if (finishing) { //结束了 --最后一个收尾线程能进入这个逻辑
                    nextTable = null; //扩容完成
                    table = nextTab; //table指向新的table表, 原table上的东西 能被GC回收
                    sizeCtl = (n << 1) - (n >>> 1); //新的阈值设置      2*n - n/2 =  3/2n  =》 2*n*0.75=2*(3/4)*n  等价的
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { //当前线程退出,帮助扩容的线程的数量减少1
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) //判断是不是最后一个线程
                        return; //不是最后一个线程 直接结束
                    finishing = advance = true; //最后一个线程去做完收尾工作就结束
                    i = n; // recheck before commit 这个时候 bound是0, i为原table长度,所以最后一个线程从尾到头进行扫描一次。
                }
            }
            else if ((f = tabAt(tab, i)) == null) //槽位为null
                advance = casTabAt(tab, i, null, fwd); //一个线程共用fwd  这里是和put方法的线程进行竞争 
            else if ((fh = f.hash) == MOVED) //已经迁移了
                advance = true; // already processed
            else { //需要进行迁移(链表或者红黑树)
                synchronized (f) { //加锁
                    if (tabAt(tab, i) == f) { //判断是不是被移除了  这里和hashmap的思想一样都是分为高桶和低桶
                        Node<K,V> ln, hn;
                        if (fh >= 0) { // 槽位上是链表结构, 整体上的算法和hashMap有点不一样
                            int runBit = fh & n;//如果hash值大于长度说明是要去高桶
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n; //是否大于原长度,0就是在低桶, 大于1就是在高桶
                                if (b != runBit) { //说明f和下一个节点是要分别分配到高位和低位
                                    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); //呃,原table现在还没有清楚,还在迁移过程中,但是i这个槽位已经迁移完成了, 所以需要一个转发节点,去新的table上去找
                            advance = true; //该槽位迁移完成
                        }
                        else if (f instanceof TreeBin) { //如果是红黑树代理点 也和hashMap差不多一样,使用树节点的双向链表结构。。
                            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) { //first指针指向的就是双向链表的头节点
                                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) : //反树化,从双向链表变成Node类型的单向链表
                                (hc != 0) ? new TreeBin<K,V>(lo) : t; //转化为红黑树, 这里有个技巧,如果原槽位上的节点要么都保留原槽位或者都去了高槽位,那么直接使用原槽位的treeBin就行了
                            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; //槽位推进
                        }
                    }
                }
            }
        }
    }

为了让扩容的时候也能get, 再槽位设计了转发几点,转发节点会指向扩容的新table, 等所有槽位都进行迁移完成之后,原table指向新table,多线程扩容小效率很高, 呃, 迁移的时候设计到链表和红黑树节点的改变所以,需要对新table的槽位进行加锁,避免别的线程通过原table表的槽位(这个槽位上是转发节点,通过转发节点可以找到新table)找到新table进行put,导致出现线程安全问题。

helpTransfer

B线程再对原table扩容的时候,另外一个线程A进行了put操作,如果key对应原table的槽位的hash值是-2,也就是这个槽位是转发节点,代表这个槽位已经迁移到新table上了,所以另外一个线程A可以去协助B进行扩容

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) && //原table不为null,并且槽位上的节点是转发节点
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { //新table不为null
            int rs = resizeStamp(tab.length); //获取一个邮戳和原tablec长度有关
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) { //正在扩容中
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0) //这些条件和addCount 里面的一样
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab); //扩容
                    break;
                }
            }
            return nextTab;
        }
        return table;//返回最新的table值, volatile
    }

tryPresize

private final void tryPresize(int size) {
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : //size大于最大容量的一半 就使用最大容量
            tableSizeFor(size + (size >>> 1) + 1); //获取容量
        int sc;
        while ((sc = sizeCtl) >= 0) { //说明table还没有扩容
            Node<K,V>[] tab = table; int n;
            if (tab == null || (n = tab.length) == 0) { //table还没有初始化
                n = (sc > c) ? sc : c;
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //进行初始化扩容
                    try {
                        if (table == tab) { //说明table没有别的线程扩容
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; //扩容
                            table = nt; //指向新table
                            sc = n - (n >>> 2);  //新的阈值
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            else if (c <= sc || n >= MAXIMUM_CAPACITY) //下于现有容量或者大于最大容量,不需要扩容
                break;
            else if (tab == table) { //还没有扩容完成
                int rs = resizeStamp(n);
                if (sc < 0) { //扩容已经开始了则加入
                    Node<K,V>[] nt;
                    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);
            }
        }
    }

replaceNode

final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode()); //hash映射
        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) //说明table再扩容, 需要协助扩容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                boolean validated = false;
                synchronized (f) { //加锁
                    if (tabAt(tab, i) == f) { //没有被remove
                        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) //新value不为null
                                            e.val = value; //替换
                                        else if (pred != null) //前驱为不空, 越过该节点
                                            pred.next = e.next;
                                        else //前驱问空,把next放入槽位
                                            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;
                                    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;
                }
            }
        }
        return null;
    }

removeTreeNode

首先进行双向链表的删除, 然后进行红黑树的删除, 呃,HashMap和ConcurrentHashMap的删除逻辑差不多,区别就是ConcurrentHashMap再对红黑树删除操作的时候需要加读锁

final boolean removeTreeNode(TreeNode<K,V> p) {
            TreeNode<K,V> next = (TreeNode<K,V>)p.next;
            TreeNode<K,V> pred = p.prev;  // unlink traversal pointers
            TreeNode<K,V> r, rl;
            if (pred == null) /*双向链表的删除*/
                first = next;
            else
                pred.next = next;
            if (next != null)
                next.prev = pred;
            if (first == null) {
                root = null;
                return true;
            }
            if ((r = root) == null || r.right == null || // too small
                (rl = r.left) == null || rl.left == null)
                return true;
            lockRoot();//红黑树的删除 加寄生写锁
            try { 
                TreeNode<K,V> replacement;
                TreeNode<K,V> pl = p.left;
                TreeNode<K,V> pr = p.right;
                if (pl != null && pr != null) { //删除节点的左右孩子都不为null
                    TreeNode<K,V> s = pr, sl;
                    while ((sl = s.left) != null) // find successor 得到中序遍历的后继
                        s = sl;
                    boolean c = s.red; s.red = p.red; p.red = c; // swap colors 交互颜色
                    TreeNode<K,V> sr = s.right;
                    TreeNode<K,V> pp = p.parent;
                    if (s == pr) { // p was s's direct parent p的右子树没有左子树
                        p.parent = s; //对调
                        s.right = p;
                    }
                    else {
                        TreeNode<K,V> sp = s.parent;
                        if ((p.parent = sp) != null) { //p指向s的parent
                            if (s == sp.left)  //p替换了s的位置
                                sp.left = p;
                            else //兼容 前前驱的情况
                                sp.right = p;
                        }
                        if ((s.right = pr) != null)//s替代p都位置
                            pr.parent = s;
                    }
                    p.left = null; //p左孩子为null
                    if ((p.right = sr) != null)
                        sr.parent = p;
                    if ((s.left = pl) != null)
                        pl.parent = s;
                    if ((s.parent = pp) == null) // 根节点
                        r = s;
                    else if (p == pp.left) /*删除非空节点*/
                        pp.left = s;
                    else
                        pp.right = s;
                    if (sr != null) //位置调整之后p有孩子
                        replacement = sr;
                    else //没有孩子
                        replacement = p;
                }
                else if (pl != null) //删除点击有孩子
                    replacement = pl;
                else if (pr != null) //删除节点有孩子
                    replacement = pr;
                else //删除节点没有孩子
                    replacement = p;
                if (replacement != p) { //说明删除节点有后继
                    TreeNode<K,V> pp = replacement.parent = p.parent; //替换节点代替 删除节点
                    if (pp == null)
                        r = replacement;
                    else if (p == pp.left)
                        pp.left = replacement;
                    else
                        pp.right = replacement;
                    p.left = p.right = p.parent = null; //删除节点 GC
                }

                root = (p.red) ? r : balanceDeletion(r, replacement); //替代节点平衡调整

                if (p == replacement) {  // detach pointers 删除节点没有左右孩子, 删除调替代节点
                    TreeNode<K,V> pp;
                    if ((pp = p.parent) != null) {
                        if (p == pp.left)
                            pp.left = null;
                        else if (p == pp.right)
                            pp.right = null;
                        p.parent = null;
                    }
                }
            } finally {
                unlockRoot(); //解锁
            }
            assert checkInvariants(root);
            return false;
        }

删除之后调整平衡和HashMap的一样

ConcurrentHashMap的删除和替换都是调用的replaceNode

compute

需求 K确定好了,但是V还没有确定好, 所以先占位

public V compute(K key,
                     BiFunction<? super K, ? super V, ? extends V> remappingFunction) {  //v为函数式接口
        if (key == null || remappingFunction == null)
            throw new NullPointerException();
        int h = spread(key.hashCode()); //hash映射
        V val = null;
        int delta = 0;
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0) //初始化table表
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & h)) == null) { //槽位上没有
                Node<K,V> r = new ReservationNode<K,V>(); //占位节点 
                synchronized (r) { //类似局部变量??? 
                    if (casTabAt(tab, i, null, r)) { //cas 把占位符塞入槽位 如果成功,别的线程可以看到槽位上的占位符
                        binCount = 1;
                        Node<K,V> node = null;
                        try {
                            if ((val = remappingFunction.apply(key, null)) != null) { //计算value
                                delta = 1;
                                node = new Node<K,V>(h, key, val, null); //创建Node
                            }
                        } finally {
                            setTabAt(tab, i, node); //替换占位符
                        }
                    }
                }
                if (binCount != 0) //说明计算得到结果了 
                    break;
            }
            else if ((fh = f.hash) == MOVED) //扩容中
                tab = helpTransfer(tab, f); //协助
            else {
                synchronized (f) { //加锁
                    if (tabAt(tab, i) == f) { //没有被删除
                        if (fh >= 0) { //链表 新增 、 删除 、 修改 都包括
                            binCount = 1;
                            for (Node<K,V> e = f, pred = null;; ++binCount) {
                                K ek;
                                if (e.hash == h &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) { //找到相同key的
                                    val = remappingFunction.apply(key, e.val); //计算新value
                                    if (val != null) //新value不为空 进行替换
                                        e.val = val;
                                    else { //新value为null , 删除操作
                                        delta = -1;
                                        Node<K,V> en = e.next;
                                        if (pred != null)
                                            pred.next = en;
                                        else
                                            setTabAt(tab, i, en);
                                    }
                                    break;
                                }
                                pred = e;
                                if ((e = e.next) == null) { //链表上没有相等的key
                                    val = remappingFunction.apply(key, null);
                                    if (val != null) { //新增
                                        delta = 1;
                                        pred.next =
                                            new Node<K,V>(h, key, val, null);
                                    }
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) { //红黑树
                            binCount = 1;
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> r, p;
                            if ((r = t.root) != null) //有根节点
                                p = r.findTreeNode(h, key, null); //开始找到
                            else
                                p = null;
                            V pv = (p == null) ? null : p.val;
                            val = remappingFunction.apply(key, pv); //计算新value
                            if (val != null) {
                                if (p != null) //替换
                                    p.val = val;
                                else { //新增
                                    delta = 1;
                                    t.putTreeVal(h, key, val);
                                }
                            }
                            else if (p != null) { //删除
                                delta = -1;
                                if (t.removeTreeNode(p))
                                    setTabAt(tab, i, untreeify(t.first)); //反树化
                            }
                        }
                    }
                }
                if (binCount != 0) { //装树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    break;
                }
            }
        }
        if (delta != 0) //要么新增节点了, 要么删除节点了
            addCount((long)delta, binCount);
        return val;
    }

clear

public void clear() { //删除, 单向链表和双向链表
        long delta = 0L; // negative number of deletions
        int i = 0; //从左到右进行删除
        Node<K,V>[] tab = table;
        while (tab != null && i < tab.length) { //table表合规
            int fh;
            Node<K,V> f = tabAt(tab, i);//获取槽位的节点
            if (f == null) //如果是null,就去下一个槽位
                ++i;
            else if ((fh = f.hash) == MOVED) { //table正在扩容
                tab = helpTransfer(tab, f); //协助扩容
                i = 0; // restart 从头重新开始删除
            }
            else { //正常节点
                synchronized (f) { //对槽位进行加锁 
                    if (tabAt(tab, i) == f) { //如果槽位上的节点没有发生改变
                        Node<K,V> p = (fh >= 0 ? f :
                                       (f instanceof TreeBin) ?
                                       ((TreeBin<K,V>)f).first : null); //是树节点还是正常链表节点
                        while (p != null) { //开始删除
                            --delta;
                            p = p.next;
                        }
                        setTabAt(tab, i++, null); //槽位置为null
                    }
                }
            }
        }
        if (delta != 0L) 
            addCount(delta, -1); //只进行计数, 不进行扩容
    }

删除直接拿链表删除就行,不需要管红黑树

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值