并发编程(五)ConcurrentHashMap源码简析

前言

ConcurrentHashMap这个集合是在面试中出现频率较高的一个集合,这个集合的源码层面体现了多处并发设计的思想,除此之外,其底层存储结构也有很多值得考量的地方,源码实现中对位运算的运用也相当成熟,因此这个集合源码有很高的学习价值,这里依旧是在大牛博客的基础上进行总结。本篇博客以1.8中的源码为例进行总结。

ConcurrentHashMap简单使用

package com.learn.concurrentSet.hashmap;

import java.util.Hashtable;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * autor:liman
 * createtime:2019/7/14
 * comment: ConcurrentHashMap实例
 */
public class ConcurrentHashMapDemo {

    private static int INPUT_NUMBER = 100000;

    public static void main(String[] args) throws InterruptedException {
//        Map map = new Hashtable<>(12 * INPUT_NUMBER);
        Map map = new ConcurrentHashMap<>(12 * INPUT_NUMBER);
        long begin = System.currentTimeMillis();
        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            service.execute(new InputWorker(map, i));
        }
        service.shutdown();
        service.awaitTermination(1, TimeUnit.DAYS);
        long end = System.currentTimeMillis();
        System.out.println("span time = " + (end - begin) + ", map size = " + map.size());
    }

    private static class InputWorker implements Runnable {
        private static Random rand = new Random(System.currentTimeMillis());
        private final Map map;
        private final int flag;

        private InputWorker(Map map, int begin) {
            this.map = map;
            this.flag = begin;
        }

        @Override
        public void run() {
            int input = 0;
            while (input < INPUT_NUMBER) {
                int x = rand.nextInt();
                if (!map.containsKey(x)) {
                    map.put(x, "liman " + x);
                    input++;
                }
            }
            System.out.println("InputWorker" + flag + " is over.");
        }
    }
}

上述实例比较简单,只是利用几个线程,往ConcurrentHashMap中存取数据。测试中会发现,HashMap效率比HashTable要快的的多。 

Hash表

ConcurrentHashMap源码分析

这里说明一点,ConcurrentHashMap可以支持并发读写,在1.8的版本中,ConcurrentHashMap取消了段(segment)的概念,但是在源码中这个还是有保留的。

ConcurrentHashMap原理概述

ConcurrentHashMap是典型的Hash表的结构,其中是通过一个Node[] 数组来维护的数据结构,具体结构如下图所示(图片是参考的某个大牛的博客,ConcurrentHashMap源码分析),同时ConcurrentHashMap是在第一次添加元素的时候才会初始化。

并发编程(五)ConcurrentHashMap源码简析_第1张图片

 具体的扩容原则我们后面再详细说明。

几个重要的类型和方法

几个属性

private static final int MAXIMUM_CAPACITY = 1 << 30;
private static final int DEFAULT_CAPACITY = 16;

static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;

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; // usable bits of normal node hash

transient volatile Node[] table; //默认没初始化的数组,用来保存元素

private transient volatile Node[] nextTable; //转移的时候的数组

/**
	这个值有多重含义
	在初始化的时候指定了长度,sizeCtl表示需要扩容的阈值,大小为数组长度的0.75
	如果为-1,表示ConcurrentHashMap正在初始化
	如果为-(1+n),n表示正在进行扩容的线程个数
*/
private transient volatile int sizeCtl;	

Node

static class Node implements Map.Entry {
    final int hash; //node的hash值
    final K key; //key值
    volatile V val; //对应的value值
    volatile Node next; //next 节点

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

TreeNode

//可以看到,这个TreeNode继承至Node,所以这里就没有了key和value
static final class TreeNode extends Node {
    TreeNode parent;  // red-black tree links
    TreeNode left;
    TreeNode right;
    TreeNode prev;    // needed to unlink next upon deletion
    boolean red;

    TreeNode(int hash, K key, V val, Node next,
             TreeNode parent) {
        super(hash, key, val, next);
        this.parent = parent;
    }
}

几个重要的方法

//用来返回数组中指定下标的节点数据,其实质也就是tab[i],只是利用cas来操作,这个是为了避免可见性问题。
static final  Node tabAt(Node[] tab, int i) {
    return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

//cas原子操作,替换指定位置上的值
static final  boolean casTabAt(Node[] tab, int i,
                                    Node c, Node v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

//设置指定位置上的值
static final  void setTabAt(Node[] tab, int i, Node v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

初始化

//如果参数是Map,则将调用putAll,同时将sizeCtl设置为默认大小
public ConcurrentHashMap(Map m) {
    this.sizeCtl = DEFAULT_CAPACITY;
    putAll(m);
}

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    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);

    //如果指定了容量,这将sizeCtl初始化为指定的cap
    this.sizeCtl = cap;
}

put操作

public V put(K key, V value) {
    return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
//onlyIfAbsent如果是true,表示只有在不存在该key时才会进行put操作
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();//如果key和value,两者中任意一个为空,则抛出异常
	
	//hash码是32位字符串,这里是取出高4位
    int hash = spread(key.hashCode());
    int binCount = 0;//用来记录相应链表的长度
	
	//自旋,将元素放入到ConcurrentHashMap中
    for (Node[] tab = table;;) {
        Node f; int n, i, fh;
		
		//如果table没有数据,或者为空,则初始化table,即第一次放入元素的时候才初始化Node数组。
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();//这里就是初始化数组
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果对应的Node数组的下标中,没有元素。
			//则用cas替换该位置上的元素。
            if (casTabAt(tab, i, null,
                         new Node(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
		//如果检测到某个节点的hash值是MOVED,则表示正在进行数组扩容的数据复制阶段。则当前线程也要参与去复制数据的功能。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {//加锁,获得数组该位置头结点的监视器锁
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {//如果去取出的元素的hash值>0,说明是链表,因为转换为树之后,节点的hash值为-2.
                        binCount = 1;//用于计数,记录链表长度,如果大于阈值,是要扩容的。
                        for (Node e = f;; ++binCount) {//遍历链表
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {//如果key和value,hash都相等,则替换该节点的value值。
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            
                            //到了链表末端,将这个新值放入到链表的最后面
                            Node pred = e;
                            if ((e = e.next) == null) {//如果不是,则加在链表的尾部。
                                pred.next = new Node(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {//这里表示,ConcurrentHashMap已经转换为红黑树了,
                        Node p;
                        binCount = 2;
						//调用putTreeVal方法,将元素放入到ConcurrentHashMap中。
                        if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)//当在同一个节点的数目达到8个的时候,则扩张数组或将给节点的数据转为tree
                    treeifyBin(tab, i);//这个方法和HashMap有所不同,如果数组的长度小于64,那么会选择进行数组扩容,而不是转换为红黑树
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);//这个是计数的方法,里面用到了MapReduce的思想,比较复杂
    return null;
}

以上的put操作可以用一下文字来总结:当添加一对键值对的时候,首先会判断保存这些键值对的数组是不是初始化了,如果没有初始化的话,就初始化这个数组,然后计算hash值来确定放在数组的那个位置,如果这个位置为空则直接添加,如果不为空的话, 则取出这个节点来,如果取出的节点的hash值是MOVED的话,则表示当前正在对这个数组进行扩容,复制到新数组,则当前线程也去帮助复制,即调用helpTransfer方法。

如果这个节点不为空,也没有扩容,则通过synchronized来加锁,进行添加操作,然后判断当前取出的节点位置存放的是链表还是树,如果是链表的话,则遍历整个链表,将取出的节点key值、value值还有hash值,如果相等,则直接覆盖,如果不等,则加载链表的末尾。如果是树的话,则用putTreeVal方法将其添加到红黑树中去。

添加完元素之后会判断当前的binCount是否大于8,如果大于8则转换成红黑树,或者扩容数组。后面的treeBin就是完成这个操作的。

上述代码几乎就是ConcurrentHashMap的put主流程,但是有几个问题我们还需要深入探讨,第一个是初始化,第二个是扩容,第三个是扩容过程中的数据迁移。

initTable

这里涉及一个关键的变量——sizeCtl,这个变量的值有多种含义。这个变量是在Node数组初始化或者扩容的时候的一个控制标志位。

sizeCtl为-1,表示正在初始化

sizeCtl为-N,表示有N-1个线程正在进行扩容。

sizeCtl为0,表示Node数组还没初始化完成。

源码如下:

private final Node[] initTable() {
    Node[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)//如果其他线程正在初始化数组。
            Thread.yield(); // lost initialization race; just spin
        
        //CAS替换sizeCtl状态,将其标记为-1,表示抢到了初始化的锁
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    //如果szieCtl>0,这个变量指示扩容的大小。否则默认的初始化容量是16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")

                    //这里就是传说中的初始化Node数组了。
                    Node[] nt = (Node[])new Node[n];
                    table = tab = nt;

                    //sc=sc*0.75,这个时候sc变成了阈值
                    sc = n - (n >>> 2);
                }
            } finally {

                //sizeCtl记录阈值
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

可以看到其实也很简单,根据sizeCtl的状态来进行初始化操作,如果sizeCtl<0表示有线程正在初始化,在初始化数组之前先替换sizeCtl的状态,之后根据sc判断初始化的数组容量,默认Node节点数组长度是16,阈值为长度的0.75。 

扩容

其实扩容分为两种,一种是Node数组的扩容,另一种是链表转红黑树的扩容。前者源码阅读起来难度较大,这里先看看后者。毕竟前置需要再hash.

 链表转红黑树:treeifyBin

putVal方法中链表转红黑树,treeifyBin扩容操作发生在如下,如下所示。binCount是用来记录当前链表的长度,如果链表中节点的个数大于TREEIFY_THRESHOLD,这个值为8,也就是说,如果链表中的节点个数大于8的时候,数据结构就有可能从链表转换成红黑树。

并发编程(五)ConcurrentHashMap源码简析_第2张图片

treeifyBin的源码分析 

private final void treeifyBin(Node[] tab, int index) {
    Node b; int n, sc;
    if (tab != null) {
        //这里还有一个判断,如果Node数组的长度小于64,则直接进行Node数组的扩容即可
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1);//后面会详细说明这个方法
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {//这里b被赋值为头结点
            synchronized (b) {
                if (tabAt(tab, index) == b) {

                    //这里就是遍历链表建立红黑树,这个在后续红黑树的总结中再详细讨论。
                    TreeNode hd = null, tl = null;
                    for (Node e = b; e != null; e = e.next) {
                        TreeNode p =
                            new TreeNode(e.hash, e.key, e.val,
                                              null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    //将红黑树设置到数组相应位置中
                    setTabAt(tab, index, new TreeBin(hd));
                }
            }
        }
    }
}

 从源码上看,如果链表中长度binCount>8并不一定会将链表转换为红黑树,如果Node数组的长度小于64,则只扩容Node数据即可。也就是说,如果链表要转换成红黑树,需要满足两个条件:1、Node数组长度大于64。2、binCount>8

tryPresize扩容

//在putVal传入进来的时候,size是已经是原来的两倍了
private final void tryPresize(int size) {

    //对size进行修复,先判断size是否大于阈值。通过tableSizeFor将传入的参数转换为大于这个数最小的2次幂
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    while ((sc = sizeCtl) >= 0) {
        Node[] tab = table; int n;
        //这段代码和initTable是一样的。table没有初始化,就初始化table
        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[] nt = (Node[])new Node[n];
                        table = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        else if (tab == table) {//这里与addCount一样
            int rs = resizeStamp(n);
            if (sc < 0) {
                Node[] 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); //扩容后的再hash操作
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}

上述基本属于链表扩容的操作,其实正真比较难的是transfer操作

数据迁移:transfer

再介绍transfer的时候,先要介绍resizeStamp

static final int resizeStamp(int n) {
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

其中的Integer.numberOfLeadingZeros(n)——返回无符号整数n最高位非0位前面的0的个数。

例如:10的二进制为:0000 0000 0000 0000 0000 0000 0000 1010 ,上述函数则返回标红的0的个数,为28。RESIZE_STAMP_BITS的默认值为16,这里就以resizeStamp(16)为实例来说明其最后的返回值。

16的二进制为:0000 0000 0000 0000 0000 0000 0001 0000。那Integer.numberOfLeadingZeros(16)会返回27,27的二进制为:0000 0000 0000 0000 0000 0000 0001 1011。这个数值再与左移1位的15二进制进行或操作即:0000 0000 0000 0000 0000 0000 0001 1011 | 0000 0000 0000 0000 1000 0000 0000 0000最后得到:0000 0000 0000 0000 1000 0000 0001 1011。

这似乎没有什么意义,具体的意义在下面一个方法中,当一个线程尝试进行扩容的时候,会执行下面一段代码:

U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))

上述的代码中的U其实就是JDK为我们封装的Unsafe类,上述的rs就是resizeStamp的返回值,这个在后面会贴出来,这个操作就是将上面的resizeStamp函数的返回的二进制左移16位,然后加2,最终得到的二进制为

1000 0000 0001 1011 0000 0000 0000 0010。这就是sizeCtl的最终结果,这里可以看出,sizeCtl的高16位记录的是扩容标记,后面记录的是扩容线程的个数

下面开始正式说明transfer方法,transfer方法是支持并发操作的,针对transfer来说,transfer中将Node数组当作多个线程之间共享的任务队列,然后通过维护一个指针(transferIndex)来划分每个线程负责的区间,每个线程通过区间逆向遍历来实现扩容,一个已经迁移完的节点会被替换为一个ForwardingNode节点,用来标记当前节点已经被其他线程迁移完毕。

//tab是原node数组,nextTab是用来扩容的数组
private final void transfer(Node[] tab, Node[] nextTab) {
    int n = tab.length, stride;

    //这里就是让每个CPU处理的任务一样多,避免出现转移任务不均匀的情况。
    //这里的stride就是步长,有n个位置需要进行迁移,每个任务迁移stride个子任务。
    //
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range

    //如果nextTab为空,就进行一次初始化,
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")

            //容量翻倍
            Node[] nt = (Node[])new Node[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;//扩容失败,则将sizeCtl初始化最大值。
            return;
        }
        nextTable = nextTab;
        transferIndex = n;//初始化迁移的位置,上述两者都是ConcurrentHashMap中的属性。
    }
    int nextn = nextTab.length;//新的tab的长度。

    //ForwardingNode表示正在被迁移的节点,其hash值为-1,其作用是用来占位,表示原数组中位置i处的节点完成迁移以后,就会在i位置设置一个fwd来告诉其他线程这个位置已经处理过了。
    ForwardingNode fwd = new ForwardingNode(nextTab);
    boolean advance = true;
    
    //判断是否已经扩容完成
    boolean finishing = false; // to ensure sweep before committing nextTab
    
    //i指向了transferIndex,bound指向了transferIndex-stride
    for (int i = 0, bound = 0;;) {
        Node f; int fh;
        while (advance) {//advance表示是否可以进入下一个Node节点进行迁移
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {//将transferIndex赋值给nextIndex,如果nextIndex<0说明原数组的所有位置都有相应的线程去处理了
                i = -1;
                advance = false;
            }
            //通过cas来替换transferIndex
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                //括号中的代码,nextBound是这次迁移的边界点,从后往前
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {//i<0表示已经遍历完旧的数组
            int sc;
            if (finishing) {//如果完成了扩容,
                nextTable = null;
                table = nextTab;//更新table数组
                sizeCtl = (n << 1) - (n >>> 1);//更新阈值
                return;
            }
            //将sizeCtl中的迁移线程数减一
            if (U.compareAndSwapInt(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 ln, hn;
                    if (fh >= 0) {//头结点的hash>0说明是链表的Node节点。
                        //下面就是将链表一分为二,找到原链表中的lashRun,然后lastRun及其之后的节点是一起迁移的,lastRun之前的节点需要进行克隆,然后分到两个链表中,这其实就是再hash的过程。
                        int runBit = fh & n;
                        Node lastRun = f;
                        for (Node 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 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(ph, pk, pv, ln);
                            else
                                hn = new Node(ph, pk, pv, hn);
                        }
                        setTabAt(nextTab, i, ln);//其中的一个链表放在新数组的位置i
                        setTabAt(nextTab, i + n, hn);//另一个链表放在新数组的位置i+n

                        
                        //将原数组该位置处设置为fwd,代表该位置已经处理完毕,
                        //其他线程一旦看到该位置的hash值为MOVED,就不会进行迁移了
                        setTabAt(tab, i, fwd);
                        advance = true;//advance设置为true,代表该位置已经迁移完毕
                    }
                    else if (f instanceof TreeBin) {//红黑树的迁移
                        TreeBin t = (TreeBin)f;
                        TreeNode lo = null, loTail = null;
                        TreeNode hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode p = new TreeNode
                                (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;
                            }
                        }

                        //如果一分为二之后,节点数少于8,那么将红黑树转换回链表
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                            (hc != 0) ? new TreeBin(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                            (lc != 0) ? new TreeBin(hi) : t;
                        //将ln放置在新数组的位置
                        setTabAt(nextTab, i, ln);
                        //将hn放置在新数组的位置i+n
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);//将原数组该位置处设置为fwd,代表该位置已经被处理过了
                        advance = true;//表示迁移已经完成
                    }
                }
            }
        }
    }
}

想要看到transfer还是需要一点精力的,后面的博客会画图进行总结,这里先写上注释。 

总结

整篇文章写的有些凌乱,但总体写出了ConcurrentHashMap的主要流程,ConcurrentHashMap中针对位运算和多线程的控制真心值得学习

参考资料:

ConcurrentHashMap 1.8源码解析

Java7/8中的HashMap源码解析

 

 

你可能感兴趣的:(多线程)