《Java并发编程的艺术》第六章Java并发容器和框架(1)ConcurrentHashMap(基于JDK11)

这一章讲得有点泛(不过讲详细了估计就得写四章不止了2333),用的JDK版本也与我的不一样(我的是11)…emmm按着书的思路,本地的JDK源码梳理吧。

ConcurrentHashMap

ConcurrentHashMap简单的说就是并发下使用的HashMap。
为啥不直接用HashMap呢?
一方面是值会丢失(这个倒是好理解,阔能出现覆盖的情况,如果俩线程盯着的是同一个地方的话,就会出现这个问题,而且如果是红黑树可能出现,虽然存了看着没丢失,但是如果再转换回链表估计还会丢几个)。

另一方面就是会引发死循环(1.7扩容时会引发循环链表,1.8据说是红黑树成环(不过我的环境至今没有复现过,或许11修复了?))。

还有一个方面就是写入后另一个线程读值,就算存进去了,大概率也是null,对其他线程不可见嘛。

除非对HashMap加锁,要不然还是老老实实的用ConcurrentHashMap吧。
那为啥不直接对HashMap加锁呢?
效率问题,HashMap直接加锁不就跟HashTable一样锁整表了嘛,而ConcurrentHashMap就不一样,在1.8之前是将原来的Node[] 分成一个一个segment,segment就是段锁。不过1.8之后是用锁每个桶中的头节点,而读是不需要加锁的(红黑树的桶是需要有个读写锁的)。其余结构与HashMap类似。

ConcurrentHashMap初始化
参数含义

天呐和HashMap比多了好多参数(代码也长了3,4倍吧大概)。
还是一点一点的啃吧。
常量:

  • MAXIMUM_CAPACITY: 最大表容量,1<<30(int类型的最大值是1<<31-1,又由于需要保持2的幂次方)
  • **DEFAULT_CAPACITY:**默认初始表容量,16
  • MAX_ARRAY_SIZE: 最大的转换位数组的大小,List所允许的最大大小,int的最大值-8(至于为啥-8.官方的解释是为了存储对象头和在一些机器上减小出错的概率(比如内存溢出))
  • DEFAULT_CONCURRENCY_LEVEL: 默认并发级别,不过JDK11未使用,仅为了兼容以前的版本。16.
  • LOAD_FACTOR: 老朋友了,负载系数,如果已经使用的桶的数量大于了总容量的0.75,那么久需要扩容了,为啥采用0.75,是空间与时间的一种权衡。
  • TREEIFY_THRESHOLD: 链表树化的阈值。为8,取这个值是因为哈希分布基本泊松分布,所以当一个桶中元素数量为8时,已经是一种比较极端的情况了,所以这时采用将链表树化,空间换时间。
  • UNTREEIFY_THRESHOLD: 树退化为链表的阈值,为6,正好是高度为2的红黑树最大能达到的值。
  • MIN_TREEIFY_CAPACITY: 最小的树化桶表容量,64.也就是说树化不仅得桶中元素大于等于8,还得表容量大于64.
  • MIN_TRANSFER_STRIDE: 扩容,转移时每个核心处理的最小的桶表数量。
  • RESIZE_STAMP_BITS: 生成每次扩容都唯一的生成戳的数,16
  • MAX_RESIZERS: 最大参与扩容的线程数,(1 << (32 - RESIZE_STAMP_BITS)) - 1 = 2^15-1;
  • RESIZE_STAMP_SHIFT: 用作扩容后生成戳位移的偏移量。
  • MOVED=-1 ForwardingNode的hash值,ForwardingNode用于在扩容时,将读操作转发到新数组上,将写操作的线程去帮忙扩容。
  • TREEBIN = -2 :TreeBin的hash值,TreeBin持有某个桶内红黑树的根节点,它存在的目的是,由于红黑树写操作会改变树结构,所以读写时需要维护一个读写锁。
  • RESERVED = -3:保留的Hash仅用于computeIfAbsent和 compute
  • HASH_BITS = 0x7fffffff 做Hash运算时与其相与,可限定Hash的范围为正。
  • NCPU: CPU核心数,也是用于扩容时算每个线程需要转移多少桶。
  • ObjectStreamField[] : 为了兼容JDK7以前版本,序列化伪字段。
这个类可以让数组像C语言的指针一样操作内存。
所以名字很直观,如非必须最好别用
private static final Unsafe U = Unsafe.getUnsafe();
    sizeCtl的偏移量
    private static final long SIZECTL;
    transferIndex的偏移量
    private static final long TRANSFERINDEX;
    baseCount的偏移量
    private static final long BASECOUNT;
    callsBusy的偏移量
    private static final long CELLSBUSY;
    cellValue的偏移量
    private static final long CELLVALUE;
    Node[]的首地址
    private static final int ABASE;
    Node[]的偏移量
    private static final int ASHIFT;

类变量:

 	transient volatile Node<K,V>[] table;
	扩容时的新数组,仅在扩容时存在,换个说法若它不为null,则说明在扩容
    private transient volatile Node<K,V>[] nextTable;
	计数器基本值,主要在没有碰到多线程竞争时使用,需要通过CAS进行更新
    private transient volatile long baseCount;
	0:默认值,此时在真正的初始化操作中使用默认容量
	> 0,初始化/扩容完成后的容量
	<0,正在进行扩容
    private transient volatile int sizeCtl;
	下一个需要迁移的桶的索引+1
    private transient volatile int transferIndex;
	CAS自旋锁标志位,用于初始化,或者counterCells扩容时
    private transient volatile int cellsBusy;
	用于高并发的计数单元,如果初始化了这些计数单元,
	那么跟table数组一样,长度必须是2^n的形式
    private transient volatile CounterCell[] counterCells;
初始化

一共有五个构造方法。
真正干活的那个方法,的执行逻辑就是
首先特判是否符合规则,
然后看并发级别是否比初始容量大,如果大了,则将初始容量置为并发级别。
再算出大小,(初始容量+1)/0.75.(+1操作减少扩容次数)
接着将大小置为最接近的2的幂次方
最后将sizeCtl设置为大小

public ConcurrentHashMap() {
    }
public ConcurrentHashMap(int initialCapacity) {
       this(initialCapacity, LOAD_FACTOR, 1);
   }   
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }


可以发现大多数调用的都是这个构造函数,我们也具体的分析一下它
public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        特判,负载参数得大于0,初始化容量得大于0,并发级别得大于0
        否则抛异常
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        如果初始容量比并发级别小,则将初始容量设为并发级别的数量
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        算一下真正的大小
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        如果大小大于了允许的最大值,则设为最大值
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }
这个求最接近且大于指定大小的2的幂次方的写法和HashMap一毛一样
就是求将-1(-1的二进制全是1,右移(31-(c-1)的最高位数)),最后+1,
则正好就是2的幂次方,至于为啥c-1,是为了防止多扩大了2倍,
正好等于2的幂次方时保留原数即可。
private static final int tableSizeFor(int c) {
        int n = -1 >>> Integer.numberOfLeadingZeros(c - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
Hash

与HashMap的类似,不过保证了Hash值为正
高低16位异或,使得更多的位数参与进来,使得在桶表中更好的散列开来。

static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
定位table中的node位置

与数组长度求余,取得坐标,然后用Unsafe类中的getObjectAcquire方法获取对应地址的Node(估计是为了可见性的同时,提高效率吧)

将hash值与桶表长度取余
tabAt(tab, (n - 1) & h))

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        使用volatile语义获取对应地址的Node
        (有点像C语言里的用指针读数组的味道)
        return (Node<K,V>)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);
    }

其他初始化

在put等操作中如果没有初始化,则回采用这个方法。
只要没有初始化初始化线程就出不去这个死循环
如果cs<0则说明有线程已经初始化并且都在扩容了,则让出时间片
如果SIZECTL为预设值,则说明还未初始化,开始尝试将其赋值,如果成功,则进入初始化流程.
失败了也没关系,说明其他线程已经初始化了,坐享其成就行了。

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            说明在扩容
            if ((sc = sizeCtl) < 0)
                线程让步,让出时间片,给扩容线程
                Thread.yield(); // lost initialization race; just spin
            获取sizeCtl,并尝试将它的值设为-1,说明正在进行初始化
            竞争不到也没关系,说明有其他线程执行了初始化
            else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
                try {
                	如果tab还未初始化
                    if ((tab = table) == null || tab.length == 0) {
                    	如果sc已经被赋值过,
                    	说明前面已经设定了数组的初始长度,
                    	则用这个值初始化数组
                    	如果为默认值(0,则用默认的容量大小初始化
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                 	将sc赋给sizeCtl,
                 	比如:当使用空参构造器时,这步就有必要了
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
操作
get

所有的操作的基础都是查找,当然先从查找开始啦

首先获取目标key的hash值
然后再判断是否有查找的价值(桶表已经初始化,长度比0大,想要查找的节点所属的桶里有节点)
如果运气贼好的头结点就是目标节点,则返回它的value.
如果它的节点类型为ForwardingNode或者为TreeNode,则用对应的方式去查找,若能找到则返回value,没有则是null。
最后一种情况就是普通链表了,直接遍历,找到则返回value.
都链表/头结点找不到或者没有找的价值返回null.

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        获取hash值
        int h = spread(key.hashCode());
        如果桶表已经初始化,长度比0大,想要查找的节点所属的桶里有节点
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            如果头结点key的哈希值等于想要查找key的哈希值
            if ((eh = e.hash) == h) {
            	如果是同一个key,或者key值相同
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                	直接返回头结点
                    return e.val;
            }
            如果头节点的hash小于0说明在ForwardingNode
            或者为红黑树节点
            else if (eh < 0)
            	这里用了多态的机制,是什么类型的节点输出什么find
            	(父类的find对其隐藏(//说到这个就气))
                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;
    }

ForwardingNode的find:
当数组正在扩容时便会将已经被迁移的节点置为ForwardingNode。
2:
首先判断是否有查找的必要,没有返回null
1:
如果正好为当前节点,则返回当前节点
如果又被扩容了,则将查找的数组再重置为新的临时数组,再重复之前操作(回到2)。
如果为树节点,则按树的方式查找
最后就是链表的情况,将当前节点置为下一个,重复1:以后所有操作,直到没有节点可查了。

Node<K,V> find(int h, Object k) {
            // loop to avoid arbitrarily deep recursion on forwarding nodes
            outer:
            在新的临时数组中查找 
            for (Node<K,V>[] tab = nextTable;;) {
                Node<K,V> e; int n;
                没有查找的价值,直接为null
                if (k == null || tab == null || (n = tab.length) == 0 ||
                    (e = tabAt(tab, (n - 1) & h)) == null)
                    return null;
                
                for (;;) {
                    int eh; K ek;
                    如果正好为头节点,返回头结点
                    if ((eh = e.hash) == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                    if (eh < 0) {
                    	如果运气贼背的又被转发了(查找时发生二次扩容了)
                        if (e instanceof ForwardingNode) {
                            将tab再置于新的临时数组,回到梦想开始的地方
                            tab = ((ForwardingNode<K,V>)e).nextTable;
                            continue outer;
                        }
                        else
                        	树节点,则按照树节点查找
                            return e.find(h, k);
                    }
                    if ((e = e.next) == null)
                        return null;
                }
            }
        }

TreeBin的find:
这个TreeBin就像TreeNode的代理一样,所有的操作由它代理了,而TreeNode指提供一个结构以及具体的查找功能
这么整的目的主要还是为了读写锁的实现方便。

说回find方法:
首先目标key不能为null
然后由于有读写锁的存在(用unsafe里的CAS操作实现的),

如果为写状态,或者等待状态则只能以链表的形式读取节点。
如果发现写完了,则尝试获取锁,并设置为读锁状态,以红黑树的方式查找,查找完后再把状态设置为读锁等待状态。

注意这是有个循环的,以链表的形式只是在当前线程等待或者无法获取时,查找以链表的方式当前节点,
下一次如果可以设置为读锁了则采用红黑树的方式,设置为读锁成功后,肯定就会返回一个值了,无论是null还是value,将当前读锁置于等待状态,最后再唤醒写线程(如果有写线程的话)。
如果还是无法设置为读锁,则还是以链表的方式读取当前节点(上一步的下一个节点)。

final Node<K,V> find(int h, Object k) {
			所要求目标key不为null
            if (k != null) {
            	e为头结点
                for (Node<K,V> e = first; e != null; ) {
                    int s; K ek;
                    如果当前锁状态为写等待状态或者写锁状态,则只能以链表的方式读取
                    if (((s = lockState) & (WAITER|WRITER)) != 0) {
       					             
                        if (e.hash == h &&
                            ((ek = e.key) == k || (ek != null && k.equals(ek))))
                            return e;
                        e = e.next;
                    }
                    尝试将当前锁状态设置为读锁
                    else if (U.compareAndSetInt(this, LOCKSTATE, s,
                                                 s + READER)) {
                        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) ==
                                (READER|WAITER) && (w = waiter) != null)
                                LockSupport.unpark(w);
                        }
                        return p;
                    }
                }
            }
            return null;
        }

TreeNode中的查询:
TreeNode里也有个find方法,不过就是调用的findTreeNode方法,这个方法和HashMap的相比只是多了一个目标key不为null的判断而已。(也就是说HashMap允许一个键为null,ConcurrentHashMap就不允许)。

put操作

put操作就没有查找那么简单了,因为涉及到扩容,扩容的写法很是巧妙呀
看了下这个方法,不仅key不能为null,value也不能。

首先判断key和value是否为null,任一为null抛异常
然后获取添加key的哈希值
再一个循环包括以下添加内容,用于失败重试

如果数组没有初始化则先初始化

如果定位的桶内没有一个节点,则尝试直接添加,如果成功就跳出循环。失败说明被其他线程抢占先机,则重试。

如果发现当前数组在扩容,则先去帮忙扩容,回来再重试

如果不允许更新,且头结点节点与添加的key有一样的key,跳出循环返回对应key的value。

然后再尝试获取该桶的头节点的锁,如果失败被阻塞,如果成功
-如果头结点未被修改
–如果为正常节点(链表),则遍历链表,直到为null,添加
–如果为TreeBin,则以红黑树的方式添加(与HashMap极其相似,仅仅是在需要改变树结构时(一些旋转平衡操作),就会加一个写锁)
–如果为ReservationNode,抛异常。

-如果是链表节点添加,则数目前的链表的长度如果大于等于8则尝试树化。
然后求得节点的数量。

final V putVal(K key, V value, boolean onlyIfAbsent) {
		如果key和value至少其一为null则抛出空指针异常
        if (key == null || value == null) throw new NullPointerException();
        获取哈希值
        int hash = spread(key.hashCode());
        
        int binCount = 0;
        获取当前数组
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh; K fk; V fv;
            如果没有初始化则初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            如果对应的桶中没有节点
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
         		则用CAS的方式,将该节点添加进去,
         		如果失败了则说明被其他线程抢先一步,
         		这时回到循环开始的地方,重新尝试添加
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                    break;                   // no lock when adding to empty bin
            }
            如果当前节点状态为被转发,也就说现在正在扩容,
            那就先去帮忙转移,
            具体怎么扩容见下
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            只添加不更新的状态下,
            检查下头节点,
            如果头节点就是想要添加进来的key,则直接返回value
            else if (onlyIfAbsent // check first node without acquiring lock
                     && fh == hash
                     && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                     && (fv = f.val) != null)
                return fv;
            else {
            
                V oldVal = null;
                获取头结点的锁
                synchronized (f) {
                	如果头节点未被修改
                    if (tabAt(tab, i) == f) {
                    	普通节点
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                找到了!与添加key一样的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);
                                    break;
                                }
                            }
                        }
                        如果为TreeBin
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            TreeBin与HashMap中的添加方式及其类似,
                            只不过在需要调整树结构时会加一个写锁。
                            如果添加失败,则保存下旧值
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                如果需要更新则更新
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                        如果为ReservationNode则抛异常,
                        这个节点只能用于
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                如添加进去了
                if (binCount != 0) {
                	如果说链表的长度大于等于阈值了,
                	则树化(当然内部会有判断容量是否大于等于64)
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        就是将值加进去,具体的后面再说
        addCount(1L, binCount);
        return null;
    }

帮助扩容:
简单的说就是:
如果能够当前有帮忙转发的需求(还有转发任务待领取和转发还未完成),重复尝试帮助。如果失败了也没关系,说明有其他线程帮忙转发了此次任务了,再尝试,直到没有转发需求。

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        需要扩容转移某些桶
        本身tab不为空,
        当前节点为转发节点
        转发节点指向的临时数组不为空
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            扩容生成戳,根据长度不同会生成唯一的生成戳,为了防止出现ABA问题
            (31-当前表长度的最高位数)|1<<(RESIZE_STAMP_BITS - 1)
            int rs = resizeStamp(tab.length);
            如果扩容还没有完成
            具体表现就是数组没有迁移完全,然后sizeCtl<0
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                如果本轮扩容结束,或者没有可以领取的扩容任务了,则跳出.
                第一个条件sc与tab.length无法对应上,
                	也就是说或许遇到ABA了,这时候就不能帮助扩容了
                第二个条件:表示本轮扩容结束
                第三个条件:表示允许的最大线程参与数已经被占满了
                第四个条件:任务已经被领取完了。
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                尝试参与转发任务
                如果失败了也没关系说明被其他线程都完成了
                if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
                	
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }

转发任务:
简单的描述就是:
首先计算本线程需要领取的任务(需要转移的桶)数,如果比预设小,则设为预设。
然后如果临时数组没有初始化,就初始化它(有一个线程初始化成功即可),预处理,根据剩余任务数,判断处理的桶的范围,从后往前分配任务和转移桶,直到完成自己的使命
最后视角转移到具体某个桶的转移工作上来
如果为链表节点,则先保存一个尾链表(就最后全为高节点,或者全为低节点的链表),然后再根据高低节点接上链表,注意使用的头插法(除了那个尾链表)。
如果为红黑树,与HashMap类似,首先分为高低子树(用链表的形式遍历),然后再根据是否能够退化为链表和是否需要重整树来调整。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        计算每个线程最少需要领取的迁移桶数,若小于最小返回则设置为最小范围
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        如果临时数组未初始化则将它初始化
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
            如果超出最大允许值了,则设置为最大值,并且不扩容了,返回
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            
            nextTable = nextTab;
            总任务数
            transferIndex = n;
        }
        int nextn = nextTab.length;
        
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        预处理
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        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;
                }
                找到用偏移量transferindex,
                尝试将它更新为本次线程领取了任务后的剩余任务
                (若不足最小领取任务,则全领了)
                由此可见,是从高到低分配任务的,转移桶
                else if (U.compareAndSetInt
                         (this, TRANSFERINDEX, nextIndex,
                          计算本线程开始转发的任务首桶所在位置索引
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    将下一个线程看到的任务位置更新
                    bound = nextBound;
                    
                    i = nextIndex - 1;
                    预处理完成
                    advance = false;
                }
            }
            如果所有转发任务都被干完了
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                如果确定已经完成了
                if (finishing) {
                	
                    nextTable = null;
                    table = nextTab;
                    设置为当前长度
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                尝试将线程数-1,表明自己退出扩容工作
                if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                	如果不是最后一个线程,直接退出
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                     如果是最后个线程则将完成的标志位设置好
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            如果头节点为空,则在该处添加转发节点
            else if ((f = tabAt(tab, i)) == null)
                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) {
                        	获取高/低位,判断方式与HashMap是一样的
                        	因为扩容后,
                        	取索引的范围大了n也就是位数多了一位
                        	如果想要知道扩容后会不会被迁移到高位
                        	只需要看这一位是否为1即可。
                            int runBit = fh & n;
                            
                            Node<K,V> lastRun = f;
                            查找到状态变化的最后一个节点
                            为了效率尽量重用节点
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            说明是低节点
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            高节点
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            遍历节点直到之前保存的都是高/低链表为止
                            也就是说除了尾链表,其他都是用的头插法,
                            会变成以前的逆序,不过因为有加锁,
                            所以无所谓啦
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                如果是低节点,创一个新节点,
                                同时接上上次的低链表
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                如果是高节点,创一个新节点
                                同时接上上次的高链表
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            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;
                            与HashMap同理串成高低链表,尾插法
                            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;
                        }
                    }
                }
            }
        }
    }
size

啊啊啊啊size写完就分析完了。

主要是依赖这个方法,获取counterCell里的值。counterCells里的值都是在每次更新数组时用CAS的方式更新,所以获取的都是最新的值。所以获取时直接相加即可。
具体的分析,下次想起时再写吧,俺懒了。

final long sumCount() {
        CounterCell[] cs = counterCells;
        long sum = baseCount;
        if (cs != null) {
            for (CounterCell c : cs)
                if (c != null)
                    sum += c.value;
        }
        return sum;
    }
remove

删除操作和HashMap差别不大,不过多了一些获取头结点锁和一些CAS比对的操作,具体的分析也是等下次想起再写吧。

很多参数的含义借鉴了这篇文章,有兴趣的朋友也阔以看看这个,写得真的很好。
详尽的JDK8的ConcurrentHashMap源码分析

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值