线程安全的ConcurrentHashMap源码走读与分析

183 阅读26分钟

文章目录


单击看大图:


在这里插入图片描述

1. 引入

之前在深入浅出的理解HsahMap的实现原理及常见面试题一文中讲到,HashMap自身在多线程高并发的场景下是无法做到线程安全的,具体来说,Jdk1.7及之前的HashMap由于采用的是头插法的方法,那么在扩容操作重新放置哈希表元素的过程中,因为多个线程之间操作的顺序不同,有可能形成循环链表,也就是死链。而Jdk1.8及之后的HashMap采用了尾插法的方式,但是它仍然无法避免多线程下的安全问题。同样在扩容操作中,可能由于多个线程之间执行顺序的不同,最后可能会造成数据的覆盖。

那么如何保证HashMap在多线程场景下的线程安全问题呢?一种简单粗暴的方法就是使用重量级的sychronized来保证线程同步。之前在线程安全的Hashtable + synchronizedMap源码剖析一文中也讲到了这种方法的具体实现,不同之处在于:

  • HashTable直接在方法上使用sychronized来进行保证,例如get()的定义如下:

    // 线程安全通过在方法上使用synchronized关键字实现
    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        // 首先获取key对应的哈希值
        int hash = key.hashCode();
        // 计算key在table中的索引
        // 取模
        int index = (hash & 0x7FFFFFFF) % tab.length;
        // 如果对应的桶存在链表,则遍历链表查找key对应的元素
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            // 如果链表中某个节点的哈希值相同并相等,返回对应的value
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        // 否则返回null
        return null;
    }
    

    其他方法的实现上同样在方法上使用了sychronized

  • Collections包下的sychronizedHashMap是在方法体中使用sychronized来进行保证,所有调用方法的线程进入到方法后,首先就需要竞争互斥锁mutex,只有竞争到了锁,才能使用HashMap中相应的方法使用资源。例如,get()的定义如下:

    public V get(Object key) {
        synchronized (mutex) {return m.get(key);}
    }
    

sychronized虽然可以通过保证有序性、一致性和可见性来实现多线程场景下的线程安全,但是它作为一种重量级锁,在锁的获取的释放过程中会引发线程的上下文切换,而上下文切换所带来的额外开销是很大。如果额外的开销带来的影响超过了它所保证的线程安全得到的优势,那么岂不是白费功夫,得不偿失。

那么,根据多线程知识以及我们的直觉,不同的线程大多数情况下并不会都集中的访问HashMap中的同一个元素,而是会访问Map中哈希表不同索引处的元素。上面的方法效率低是因为我们对于整个哈希表进行锁的管理,无论你是读取还是写入,是访问哈希表中的哪一个元素,首先都需要对整张表进行加锁,执行完再释放锁。

如果对于哈希表再多做一层操作,将整个哈希表进行分段处理,每一段进行锁的管理。如果分段合理,那么元素就可以均匀的分布到不同的段中,那么不同的线程访问不同的段需要获取的就是不同的锁,自然不会出现线程安全问题。即使多个线程同时访问同一个段的元素,由于只会对该段进行加锁和释放锁,开销只有 1 N \frac{1}{N} N1​(N为段的个数)。

那么Java中有没有这种想法的落地实现呢?自然是有的,它其实就是Jdk1.7中的ConcurrentHashMap的实现逻辑,当然细节部分有所不同,但整体的思想是一致的。下面我们就从源码的方向来看一下,它是如何实现上面的想法的,以及它和HashMap的实现具体有何不同。


2. ConcurrentHashMap in Jdk 1.7

2.1 底层结构

Jdk 1.7中的ConcurrentHashMap底层同样采用的还是和HashMap相同的数组 + 链表的实现方式,不同之处在于和链表直接相关的是HashEntry数组,而管理HashEntry数组的是多加的Segment数组,即上面所说的分段的具体实现。底层的结构图如下所示:

image-20200822162359874

单独的一个segment拿出来就相当于一个HashMap,不过它包含原来完整HashMap的一部分元素。

2.2 类定义

ConcurrentHashMap位于Java.util.concurrent包下,它的定义如下:

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
    implements ConcurrentMap<K, V>, Serializable {
}

可以看到,它继承了父类AbstractMap,并且实现了ConcurrentMap这个接口,它的定义如下:

public interface ConcurrentMap<K, V> extends Map<K, V> {
    V putIfAbsent(K key, V value);
    boolean remove(Object key, Object value);
    boolean replace(K key, V oldValue, V newValue);
    V replace(K key, V value);
}

接口中只定义了如上的四个方法,具体的实现逻辑还是看ConcurrentHashMap对象的方法。

2.3 字段(属性)

接着看一下ConcurrentHashMap关于字段的定义,如下所示:

// 序列化ID
private static final long serialVersionUID = 7249069246763182397L;

//默认初始化容量指定为16,这里和HashMap是一致的
static final int DEFAULT_INITIAL_CAPACITY = 16;

//默认加载因子也是0.72,那么扩容阈值就是 16 * 0.75 = 12
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//默认的并发级别为16,它的值和segment数量是对相应的
static final int DEFAULT_CONCURRENCY_LEVEL = 16;

//最大的容量和HashMap也是一样的
static final int MAXIMUM_CAPACITY = 1 << 30;

//单个Segment中table数组的最小长度为2
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

//Segment数量的最大值
static final int MAX_SEGMENTS = 1 << 16; 

//加锁前最大的尝试次数,具体使用可见后续的方法分析。
static final int RETRIES_BEFORE_LOCK = 2;

//segment掩码值,用于寻找对应的segment的数组索引
final int segmentMask;

//它和 segmentMask 配合用于寻找对应的segment的数组索引
final int segmentShift;

// Segment数组
final Segment<K,V>[] segments;

// 存储数据的结构,和HashMap相同
transient Set<K> keySet;
transient Set<Map.Entry<K,V>> entrySet;
transient Collection<V> values;

这里和HashMap中一样,为了后续计算哈希表索引的方便,哈希表的容量只能是2的幂次。除了上面常量字段的定义,我们还需要找到底层结构中segment数组和HashEntry数组的具体定义。

其中segment的源码定义如下:

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    private static final long serialVersionUID = 2249069246763182397L;
	
    // 最大重试次数
    static final int MAX_SCAN_RETRIES =
        Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1; // 获取当前可用的处理器的数量,若大于1,则返回64,否则返回1
    
    // 使用volatile修饰的HashEntry,对应于HashMap中的table
    ransient volatile HashEntry<K,V>[] table;
    
    // 记录segment中元素的个数
    transient int count;
	
    // 记录修改次数,之前也见到了,朱勇用于fast-fail机制
    transient int modCount;
    
    //扩容阈值
    transient int threshold;
	
    //加载因子
    final float loadFactor;
	
    // 带参构造
    Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
        this.loadFactor = lf;
        this.threshold = threshold;
        this.table = tab;
    }
    
    // 具体方法的实现
}

Segment是ConcurrentHashMap内部的一个内部类,它的实现继承了ReentrantLock,所以内部实现会使用到可重入锁的机制。其中,Segment内部需要HashEntry的支持,它的实现如下:

static final class HashEntry<K,V> {
    final int hash;
    final K key;
    // value和next指针都是用volatile修饰来保证可见性和重排序
    volatile V value;
    volatile HashEntry<K,V> next;
	
    // 全参构造
    HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
	
    // 方法的实现调用了Unsafe中的putOrderedObject来保证操作的原子性
    final void setNext(HashEntry<K,V> n) {
        UNSAFE.putOrderedObject(this, nextOffset, n);
    }

    // Unsafe mechanics
    static final sun.misc.Unsafe UNSAFE;
    // 偏移量
    static final long nextOffset;
    // 静态代码块用于获取Unsafe的实例,以及next对应的偏移量offset
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class k = HashEntry.class;
            nextOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("next"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

// 获取HashEntry指定位置的元素
@SuppressWarnings("unchecked")
static final <K,V> HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i) {
    // 如果对应位置为null,则返回null
    // 否则调用Unsafe的getObjectVolatile来获取对应位置的元素
    return (tab == null) ? null :
    (HashEntry<K,V>) UNSAFE.getObjectVolatile
        (tab, ((long)i << TSHIFT) + TBASE);
}

// 设置指定位置的元素,调用的是putOrderedObject方法
static final <K,V> void setEntryAt(HashEntry<K,V>[] tab, int i,
                                   HashEntry<K,V> e) {
    UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);
}

// 计算哈希值
private int hash(Object k) {
    int h = hashSeed;

    if ((0 != h) && (k instanceof String)) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    h += (h <<  15) ^ 0xffffcd7d;
    h ^= (h >>> 10);
    h += (h <<   3);
    h ^= (h >>>  6);
    h += (h <<   2) + (h << 14);
    return h ^ (h >>> 16);
}

HashEntry同样采用了内部类的实现方式,其中方法的实现都是通过调用Unsafe中的方法来实现的,这样可以保证方法执行的原子性,从而保证整体的线程安全。关于Unsafe的介绍,后续专门写一篇文章进行解读,这里就认为它可以以原子操作的方式实现HashEntry想要的功能即可。

2.4 构造方法

ConcurrentHashMap提供了五个构造函数,如下所示:

public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}

public ConcurrentHashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}

public ConcurrentHashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}

public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY),
         DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    putAll(m);
}

上面四个构造函数最终调用的都是下面这个构造函数,如下所示:

/**
* initialCapacity :初始容量
* loadFactor:加载因子
* concurrencyLevel:并发级别
*/
@SuppressWarnings("unchecked")
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    // 首先做参数合法化判断,不合法直接抛异常
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    
    // 如果并发级别超过了设置的MAX_SEGMENTS,那么最大只能是MAX_SEGMENTS
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    
    // 偏移量,用于计算元素所在segment的下标
    int sshift = 0;
    // 用于设定最终Segment数组的长度
    int ssize = 1;
    // 如果Segment长度小于并发级别,需要对sshift和ssize进行操作
    while (ssize < concurrencyLevel) {
        ++sshift;  // sshift自增1
        ssize <<= 1;  // ssize左移一位
    }
    // segmentShift 默认是 32 - 4 = 28
    this.segmentShift = 32 - sshift;
    // segment掩码值的设定
    // segmentMask 默认是 15 即 0000 0000 0000 1111
    this.segmentMask = ssize - 1;
    
    // 设置初始容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    // 用于确定单个Segment的容量
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
    //创建长度为cap的HashEntry数组
    //创建一个Segment对象,保存到S0对象中,后续会使用S0作为原型对象去创建对应的Segment
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    // 调用putOrderedObject来将s0放到Segment数组中
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

构造函数的中关于Segment的创建中可以看出,这里只是创建了数组中一个Segment对象,并没有把整个Segment数组初始化完毕。最终初始化效果如下所示:

image-20200822200054601

2.5 put

在具体看ConcurrentHashMap中put()的实现逻辑之前,我们首先看回想一下HashMap中put()的实现过程,基本流程如下:

  • 根据传入的key计算哈希值,然后使用哈希值得到它在table中的索引
  • 如果此时索引位置为null,则直接插入元素
  • 否则需要遍历索引位置可能存在的链表,那么需要看是否存在key相同进行值覆盖的情况,如果没有则使用头插法插入到链表中

那么ConcurrentHashMap中采用了Segment的分段锁的机制,那么首先就需要根据key得到哈希值,然后根据哈希值找到它在Segment数组和HashEntry数组中的位置,最终将元素插入到合适的位置上。那么,具体它是如何实现线程安全的,请看下面的源码:

@SuppressWarnings("unchecked")
public V put(K key, V value) {
    // 首先新建一个Segment对象
    Segment<K,V> s;
    // ConcurrentHashMap中不允许key或value为null,和HashMap是不同的
    if (value == null)
        throw new NullPointerException();
    // 根据key计算哈希值
    int hash = hash(key);
    // 根据前面定义的segmentShift和segmentMask进行按位与操作,计算key在Segment数组中的位置
    // 按位与和取模操作效果是相同的,但速度更快
    int j = (hash >>> segmentShift) & segmentMask;
    // 调用getObject找到Segment数组中位置为j的元素,getObject最终使用CAS可以得到j的最新位置
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
       // 初始化下标为j的Segment对象
        s = ensureSegment(j);
    // 在上面定义的Segment独享中添加元素
    return s.put(key, hash, value, false);
}

其中ensureSegment()用于创建一个新的Segment对象,它的源码如下:

@SuppressWarnings("unchecked")
private Segment<K,V> ensureSegment(int k) { // k就是前面计算得到的segment数组的位置
    final Segment<K,V>[] ss = this.segments;
    // k的偏移量
    long u = (k << SSHIFT) + SBASE; // raw offset
   
    Segment<K,V> seg;
    // 调用getObjectVolatile从主内存中获取最新位置的Segment对象,判断是否为null
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        Segment<K,V> proto = ss[0]; // use segment 0 as prototype
        // 常规字段定义
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
        // 创建对应的HashEntry数组
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // 双重检查,保证从主内存得到的Segment对象是最新的
             // 创建一个Segment对象s
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            // 循环检查k位置的Segment对象是否为null,如果不为null,说明其他线程已经创建成功,并且主内存中已经刷新,直接取出并返回
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                // 这里调用了compareAndSwapObject以CAS的方式来获取当前主内存中最新的对象
                // 如果成功获取则跳出循环,否则持续进行自旋
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    // 最终返回Segment对象
    return seg;
}

在前面字段的定义中,DEFAULT_INITIAL_CAPACITYDEFAULT_CONCURRENCY_LEVEL都是16

// 偏移量,用于计算元素所在segment的下标
int sshift = 0;
// 用于设定最终Segment数组的长度
int ssize = 1;
// 如果Segment长度小于并发级别,需要对sshift和ssize进行操作
while (ssize < concurrencyLevel) {
    ++sshift;  // sshift自增1
    ssize <<= 1;  // ssize左移一位
}
this.segmentShift = 32 - sshift;
// segment掩码值的设定
this.segmentMask = ssize - 1;

int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
    ++c;
// 用于确定单个Segment的容量
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
    cap <<= 1;

根据构造函数中的逻辑可以得到sshift和ssize最终分别为4和16,c等于16 / 16 = 1,cap等于2。那么,segmentShift就等于28,segmentMask等于15。因此,可以明白int j = (hash >>> segmentShift) & segmentMask;这步操作首先将哈希值右移28位,使用原本的高4位和15的二进制表示1111进行按位与,最终得到的结果范围为[0000 ~ 1111],它是一个0~15之间的数字,和之前定义的并发级别是对应的。

image-20200822200720352

上面的put()只是找到了对应的Segment数组中的位置,并新建了Segment对象,具体网Segment对象中放元素的操作由s.put(key, hash, value, false);实现,其中的put()就定义在前面的内部类Segment中,源码如下:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 因为Segment继承了ReentrantLock,所以首先调用tryLock尝试获取sengment分段锁
    // 如果加锁成功,返回null,创建HashEntry节点指向null;否则进入scanAndLockForPut方法
    // 在尝试期间, 还可以顺便看该节点在链表中有没有, 如果没有顺便创建出来
    HashEntry<K,V> node = tryLock() ? null :
    scanAndLockForPut(key, hash, value);
    V oldValue;
    // 真正的操作逻辑
    try {
        // 获取当前Segment对象中的HashEntry数组
        HashEntry<K,V>[] tab = table;
        // 获取元素在HashEntry数组中的位置
        int index = (tab.length - 1) & hash;
        // 由于可能存在链表,所以首先获取HashEntry当前位置的第一个节点
        HashEntry<K,V> first = entryAt(tab, index);
        // 遍历
        for (HashEntry<K,V> e = first;;) {
            // 如果第一个节点不为null,说明当前位置之前已有元素
            if (e != null) {
                K k;
                // 如果第一个节点和当前要插入的hashEntry节点相同,那么进行值的替换
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                // 否则,继续往下找
                e = e.next;
            }
            // 如果当前位置的第一个节点就为null,说明之前该位置没有元素存放
            else {
                // 如果节点不为null ,使用头插法插入元素
                if (node != null)
                    node.setNext(first);
                else
                    // 否则新建一个HashEntry节点,然后使用头插法插入
                    node = new HashEntry<K,V>(hash, key, value, first);
                // 元素个数加1
                int c = count + 1;
                // 如果当前Segment中元素个数已经超过了扩容阈值,而且HashEntry数组长度小于最大容量,出发扩容机制
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    // 否则,将当前节点设置为该位置的头结点
                    setEntryAt(tab, index, node);
                // 修改次数加1
                ++modCount;
                // 更新元素个数计数
                count = c;
                // 因为当前位置没有元素,oldValue为null
                oldValue = null;
                break;
            }
        }
    } finally {
        // 如果put成功,则释放锁
        unlock();
    }
    return oldValue;
}

如果上面的操作在第一步加锁成功,那么执行其中的操作,最后释放锁,如果尝试获取锁失败,那么跳入scanAndLockForPut(),它在第一次尝试获取锁失败后就会一直循环尝试继续获取锁,它的源码如下:

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    // 根据hash值定位到它对应的HashEntry数组的下标位置,并找到链表的第一个节点
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    // 设置重试次数为-1
    int retries = -1; // negative while locating node
    // 循环尝试获取锁
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        // 如果尝试次数小于0
        if (retries < 0) {
            //若 e 节点和 node 都为空,则预测性的创建一个 node 节点
            if (e == null) {
                if (node == null) // speculatively create node
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            //如当前遍历到的 e 节点不为空,则判断它的key是否等于传进来的key,若是则把 retries 设为0
            else if (key.equals(e.key))
                retries = 0;
            else
                //否则,继续向后遍历节点
                e = e.next;
        }
        // 如果尝试次数到达了最大值,那么采用lock阻塞式的获取锁,直到获取到锁才break出循环
        // 否则一直在队列中
        else if (++retries > MAX_SCAN_RETRIES) {
            lock();
            break;
        }
        // 若 retries 的值为偶数,并且从内存中再次获取到最新的头节点,判断若不等于first
  		//则说明有其他线程修改了当前下标位置的头结点,于是需要更新头结点信息
        else if ((retries & 1) == 0 &&
                 (f = entryForHash(this, hash)) != first) {
            //更新头结点信息,并把重试次数重置为 -1,继续下一次循环,从最新的头结点遍历当前链表
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    // 最后返回预先创建的node节点
    return node;
}

根据整体的实现逻辑可知,put()在执行之前需要获取可重入锁:

  • 如果第一次尝试获取锁就成功,则执行下面的操作
  • 如果第一次尝试获取锁不成功,那么需要不断循环尝试再次获取锁,循环过程中并不是阻塞的
  • 如果尝试次数达到最大次数还不能得到锁,则放弃尝试,直接进入队列等待,直到有其他线程释放锁,它就可以获取锁进行下面的操作
  • 最终都需要使用unlock方法来释放锁

由此可见,不同的线程可以都进入某个Segment,但是如果想操作其中的HashEntry数组,只有获得锁的线程才能进行,这也就是分段锁的表现所在。

2.6 get

get 时并未加锁,用了 UNSAFE 方法保证了可见性,扩容过程中,get 先发生就从旧表取内容,get 后发生就从新表取内容。get()的源码如下:

public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    // 计算索引位置
    int h = hash(key);
    // 从主内存中获取最新的Segment对象 
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        //若Segment不为空,且链表也不为空,则遍历查找节点
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
             (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
            K k;
            // 返回对应的value
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    // 否则返回null
    return null;
}

2.6 remove

public V remove(Object key) {
    int hash = hash(key);
    //定位到Segment
    Segment<K,V> s = segmentForHash(hash);
    //若 s为空,则返回 null,否则执行 remove
    return s == null ? null : s.remove(key, hash, null);
}

public boolean remove(Object key, Object value) {
    int hash = hash(key);
    Segment<K,V> s;
    return value != null && (s = segmentForHash(hash)) != null &&
        s.remove(key, hash, value) != null;
}

final V remove(Object key, int hash, Object value) {
    // 操作Segment,所需也需要加锁
    //尝试加锁,若失败,则执行 scanAndLock ,此方法和 scanAndLockForPut 方法类似
    if (!tryLock())
        scanAndLock(key, hash);
    V oldValue = null;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        //从主内存中获取对应 table 的最新的头结点
        HashEntry<K,V> e = entryAt(tab, index);
        HashEntry<K,V> pred = null;
        while (e != null) {
            K k;
            HashEntry<K,V> next = e.next;
            //匹配到 key
            if ((k = e.key) == key ||
                (e.hash == hash && key.equals(k))) {
                V v = e.value;
                // value 为空或者 value 也匹配成功
                if (value == null || value == v || value.equals(v)) {
                    if (pred == null)
                        setEntryAt(tab, index, next);
                    else
                        pred.setNext(next);
                    // 更新计数值
                    ++modCount;
                    --count;
                    oldValue = v;
                }
                break;
            }
            pred = e;
            e = next;
        }
    } finally {
        // 释放锁
        unlock();
    }
    return oldValue;
}

2.7 size

size()用于统计这个Segment数组中元素的个数,但是多线程场景下,在统计个数的同时,put和remove等改变ConcurrentHashMap结构的操作也可能会有,下面我们看一下它是如何解决这歌问题的。源码如下:

public int size() {
    //segment数组
    final Segment<K,V>[] segments = this.segments;
    //统计所有Segment中元素的总个数
    int size;
    //如果size大小超过32位,则标记为溢出为true
    boolean overflow; 
    //统计每个Segment中的 modcount 之和
    long sum;         
    //上次记录的 sum 值
    long last = 0L;   
    //重试次数,初始化为 -1
    int retries = -1; 
    try {
        // 乐观尝试
        for (;;) {
            //如果超过重试次数,则不再重试,而是把所有Segment都加锁,再统计 size
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    //强制对每一个Segment对象都使用lock进行加锁
                    ensureSegment(j).lock(); 
            }
            sum = 0L;
            size = 0;
            overflow = false;
            //遍历所有Segment
            for (int j = 0; j < segments.length; ++j) {
                Segment<K,V> seg = segmentAt(segments, j);
                //若当前遍历到的Segment不为空,则统计它的 modCount 和 count 元素个数
                if (seg != null) {
                    //累加当前Segment的结构修改次数,如put,remove等操作都会影响modCount
                    sum += seg.modCount;
                    int c = seg.count;
                    //若当前Segment的元素个数 c 小于0 或者 size 加上 c 的结果小于0,则认为溢出
                    //因为若超过了 int 最大值,就会返回负数
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            //当此次尝试统计的sum和上次统计的值相同,则说明这段时间内,并没有任何一个 Segment 的结构发生改变,就可以跳出循环,直接返回最后的统计结果
            if (sum == last)
                break;
            //如果不相等,则说明有 Segment 结构发生了改变,则将当前统计的sum赋给last,继续尝试统计
            last = sum;
        }
    } finally {
        //如果超过了指定重试次数,则说明表中的所有Segment都被加锁了,因此需要把它们都解锁
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    //若结果溢出,则返回 int 最大值,否则正常返回 size 值 
    return overflow ? Integer.MAX_VALUE : size;
}

由于在不超出最大尝试次数的情况下可以得到正确的统计值,或者超过了最大尝试次数后,会对每个Segment对象强制加锁,因此,最终得到的元素总个数是一个准确的值。

2.8 rehash

最后,我们来看一下和扩容相关的rehash(),源码如下:

//node为创建的新节点
private void rehash(HashEntry<K,V> node) {
    //当前Segment中的旧的HashEntry数组
    HashEntry<K,V>[] oldTable = table;
    //旧的容量
    int oldCapacity = oldTable.length;
    //新容量为旧容量的2倍,这和HashMap扩容是相同的
    int newCapacity = oldCapacity << 1;
    threshold = (int)(newCapacity * loadFactor);
    
    //创建新的HashEntry数组
    HashEntry<K,V>[] newTable =
        (HashEntry<K,V>[]) new HashEntry[newCapacity];
    //由于数组大小改变,位置的计算规则改变,因此需重新计算掩码
    int sizeMask = newCapacity - 1;
    //遍历旧表
    for (int i = 0; i < oldCapacity ; i++) {
        // 获取当前位置的HashEntry节点
        HashEntry<K,V> e = oldTable[i];
        //如果当前节点不为空,说明当前链表不为空
        if (e != null) {
            // 获取下一个节点
            HashEntry<K,V> next = e.next;
            //计算当前节点在新HashEntry数组中的位置
            int idx = e.hash & sizeMask;
            //如果当前节点不为空,但它的下一个节点为空,则说明这条链表只有一个节点,直接把这个节点放到新数组的对应下标位置即可
            if (next == null)   
                newTable[idx] = e;
            //否则,处理当前链表的节点迁移操作
            else { 
                //记录上一次遍历到的节点
                HashEntry<K,V> lastRun = e;
                //对应上一次遍历到的节点在新数组中的新下标
                int lastIdx = idx;
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    //计算当前遍历到的节点的新下标
                    int k = last.hash & sizeMask;
                    //若 k 不等于 lastIdx,则说明此次遍历到的节点和上次遍历到的节点不在同一个下标位置
                    //需要把 lastRun 和 lastIdx 更新为当前遍历到的节点和下标值。
                    //若相同,则不处理,继续下一次 for 循环。
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }
                //把和 lastRun 节点的下标位置相同的链表最末尾的几个连续的节点放到新数组的对应下标位置
                newTable[lastIdx] = lastRun;
                //再把剩余的节点,复制到新数组
                //从旧数组的头结点开始遍历,直到 lastRun 节点,因为 lastRun节点后边的节点都已经迁移完成了。
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    V v = p.value;
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    //用的是复制节点信息的方式,并不是把原来的节点直接迁移,区别于lastRun处理方式
                    // 这里直接新建了一个HashEntry节点,旧节点会被GC掉
                    newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                }
            }
        }
    }
    //所有节点都迁移完成之后,再处理传进来的新的node节点,把它头插到对应的下标位置
    int nodeIndex = node.hash & sizeMask; // add the new node
    //头插node节点
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    //更新当前Segment的table信息
    table = newTable;
}

3. ConcurrentHashMap in Jdk 1.8

ConcurrentHash不再使用分段锁的思想,而且进一步的缩小了锁的力度,将锁直接加载了哈希表的头节点上,同时这里使用sychronized替换了ReentrantLock。由于Jdk 1.6以后对于sychronized的不断优化,引入了轻量级锁、偏向锁、锁消除和锁膨胀等机制,使得使用sychronized所带来的线程上下文切换的开销变得很低,因此效率还是很高的。

3.1 底层结构

Jdk 1.8中ConcurrentHashMap的底层结构和HashMap是一样的,同样采用了哈希表 + 链表(红黑树)的结构,如下所示:
在这里插入图片描述

3.2 定义 + 字段

ConcurrentHashMap的定义和其中重要的字段设置如下:

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
	
    // 常规设置
    private static final int MAXIMUM_CAPACITY = 1 << 30;
    private static final int DEFAULT_CAPACITY = 16;
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    private static final float LOAD_FACTOR = 0.75f;
    static final int TREEIFY_THRESHOLD = 8;
    static final int UNTREEIFY_THRESHOLD = 6;
    static final int MIN_TREEIFY_CAPACITY = 64;
    private static final int MIN_TRANSFER_STRIDE = 16;
    private static int RESIZE_STAMP_BITS = 16;
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

    /*
     * Encodings for Node hash fields. See above for explanation.
     */
    static final int MOVED     = -1; // hash for forwarding nodes
    static final int TREEBIN   = -2; // hash for roots of trees
    static final int RESERVED  = -3; // hash for transient reservations
    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
    static final int NCPU = Runtime.getRuntime().availableProcessors();

    // 默认为 0
    // 当初始化时, 为 -1
    // 当扩容时, 为 -(1 + 扩容线程数)
    // 当初始化或扩容完成后,为 下一次的扩容的阈值大小
    private transient volatile int sizeCtl;
    // 整个 ConcurrentHashMap 就是一个 Node[],Node定义为内部类,和HashMapy一样
    static class Node<K,V> implements Map.Entry<K,V> {}
    // hash表
    transient volatile Node<K,V>[] table;
    // 扩容时的新hash表
    private transient volatile Node<K,V>[] nextTable;
    // 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
    static final class ForwardingNode<K,V> extends Node<K,V> {}
    // 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
    static final class ReservationNode<K,V> extends Node<K,V> {}
    // 作为 treebin 的头节点, 存储 root 和 first
    static final class TreeBin<K,V> extends Node<K,V> {}
    // 作为 treebin 的节点, 存储 parent, left, right
    static final class TreeNode<K,V> extends Node<K,V> {}
}

3.2 构造方法

public ConcurrentHashMap(int initialCapacity) {
    // 参数检查
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    // tableSizeFor 仍然是保证计算的大小是 2^n, 即 16,32,64 ...
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

这里同样采用了懒汉式,table只有在第一次使用时才会被创建。

3.3 get

get()的源码如下:

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // spread 方法能确保返回结果是正数
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 如果头结点已经是要查找的 key,直接返回value
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // hash 为负数表示该 bin 在扩容中或是 treebin, 这时调用 find 方法来查找
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        // 正常遍历链表, 用 equals 比较
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

ConcurrentHashMap相比于同样是线程安全的HashTable相率更高的一个原因就是,在使用get()读数据时不需要加锁,只有读到的值为null是才会加锁重读。之所以不需要加锁,是因为需要读取的数据如HashEntry数组的Node的value,或者Segment的大小值count都使用volatile修饰,保证数据的可见性,同时支持多线程同时读。

private transient volatile ConcurrentHashMap.Node<K, V>[] nextTable;
private transient volatile long baseCount;
private transient volatile int sizeCtl;
private transient volatile int transferIndex;
private transient volatile int cellsBusy;
    
    
static class Node<K, V> implements Entry<K, V> {
        final int hash;
        final K key;
        volatile V val;
        volatile ConcurrentHashMap.Node<K, V> next;
}

3.4 put

其中put()的源码如下:

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

它实际上调用的是putval(),源码如下:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // key和value都不允许为null
    if (key == null || value == null) throw new NullPointerException();
    // 其中 spread 方法会综合高位低位, 具有更好的 hash 性
    // 获取哈希值
    int hash = spread(key.hashCode());
    //用来计算当前链表上的元素个数
    int binCount = 0;
    
    for (Node<K,V>[] tab = table;;) {
        // f 是链表头节点
        // fh 是链表头结点的 hash
        // i 是链表在 table 中的下标
        Node<K,V> f; int n, i, fh;
        // 如果表为空,说明还没有初始化
        if (tab == null || (n = tab.length) == 0)
            // 初始化 table 使用了 cas, 无需 synchronized 创建成功, 进入下一轮循环
            tab = initTable();
        // 如果表已经初始化,则找到key所在的位置,判断是否为空
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 如果当前文职为空,则将新节点插入到当前位置
            // 这里插入链表头节点使用了cas, 无需synchronized
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;
        }
        // 帮忙扩容
        // 若所在位置不为空,则判断节点的 hash 值是否为 MOVED(值是-1)
        else if ((fh = f.hash) == MOVED)
            //若为-1,说明当前数组正在进行扩容,则需要当前线程帮忙迁移数据
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 锁住链表头节点
            synchronized (f) {
                // 再次确认链表头节点没有被移动
                if (tabAt(tab, i) == f) {
                    //如果hash值大于等于0,说明是正常的链表结构
                    if (fh >= 0) {
                        binCount = 1;
                        // 遍历链表,从头结点开始遍历,每遍历一次,binCount计数加1
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 找到相同的 key
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                // 更新
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            // 已经是最后的节点了, 新增 Node, 追加至链表尾
                            // 这里同样采用了尾插法
                            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;
                        // putTreeVal 会看 key 是否已经在树中, 是, 则返回对应的 TreeNode
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                              value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
                // 释放链表头节点的锁
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    // 如果链表长度 >= 树化阈值(8), 进行链表转为红黑树
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 增加 size 计数
    addCount(1L, binCount);
    return null;
}

其中,当判断到当前位置的表为空时,需要调用initTable()来初始化表,源码如下:

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    //  //循环判断表是否为空,直到初始化成功为止。
    while ((tab = table) == null || tab.length == 0) {
        // 如果sizectl小于0,说明当前有线程正在初始化操作,值的大小为线程的个数
        if ((sc = sizeCtl) < 0)
            // sizeCtl初始化为-1,表为空,不会触发扩容,当前线程放弃 CPU 时间片,只是自旋
            Thread.yield();
        // 尝试将 sizeCtl 设置为 -1(表示初始化 table),表示当前线程正在进行初始化操作
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            // 获得锁, 创建 table, 这时其它线程会在 while() 循环中 yield 直至 table 创建
            try {
                // 重新检查表是否为空
                if ((tab = table) == null || tab.length == 0) {
                    //如果sc大于0,则为sc,否则返回默认容量 16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    // 创建数组
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    //n减去 1/4 n ,即为 0.75n ,表示扩容阈值
                    sc = n - (n >>> 2);
                }
            } finally {
                //更新 sizeCtl 为扩容阈值
                sizeCtl = sc;
            }
            //若当前线程初始化表成功,则跳出循环。其它自旋的线程因为判断数组不为空,也会停止自旋
            break;
        }
    }
    return tab;
}

put操作最后调用了addCount(),用于将整个table元素个数加1,源码如下:

//线程被分配到的格子
@sun.misc.Contended static final class CounterCell {
 //此格子内记录的 value 值
    volatile long value;
    CounterCell(long x) { value = x; }
}


// check 是之前 binCount 的个数
private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    if (
        // 已经有了 counterCells, 向 cell 累加
        (as = counterCells) != null ||
        // 还没有, 向 baseCount 累加
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)
    ) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        if (
            // 还没有 counterCells
            as == null || (m = as.length - 1) < 0 ||
            // 还没有 cell
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            // cell cas 增加计数失败
            !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
        ) {
            // 创建累加单元数组和cell, 累加重试
            fullAddCount(x, uncontended);
            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) {
            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;
                // newtable 已经创建了,帮忙扩容
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            // 需要扩容,这时 newtable 未创建
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

总之,put操作的整体逻辑为:

  • 插入元素前先会判断是否需要扩容,即需要插入位置的那个Segment里的HashEntry数组是否超过了容量限制。如果超过了容量限制,则会将对应的Segment中的HashEntry数组扩容为之前的两倍,并将旧HashEntry数组中的元素转移到新数组中

  • 通过hash定位Node[]数组的索引坐标,是否有Node节点。如果没有,则使用CAS进行添加(链表的头结点),添加失败则进入下次循环

  • 检查到内部正在扩容,如果正在扩容,就帮助它一块扩容

  • 如果f!=null,则使用synchronized锁住f元素(链表/红黑二叉树的头元素)

    • 如果是Node(链表结构)则执行链表的添加操作
    • 如果是TreeNode(树型结果)则执行树添加操作
  • 判断链表长度已经达到临界值8,就需要把链表转换为树结构

3.5 size

size 计算实际发生在 putremove改变集合元素的操作之中

  • 没有竞争发生,向 baseCount累加计数

  • 有竞争发生,新建 counterCells,向其中的一个 cell 累加计数

    • counterCells 初始有两个 cell
    • 如果计数竞争比较激烈,会创建新的 cell 来累加计数
public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}
final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    // 将 baseCount 计数与所有 cell 计数累加
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

ConcurrentHashMap在统计元素的大小时,首先先通过CAS尝试2次在不加锁的情况下统计各个Segment大小,如果统计的过程中没有发生变化(根据modCount的值是否发生改变判断),那么统计得到的结果就是真实的计数,减少了加锁释放锁的开销。如果统计过程中值值发生了改变,则对每个Segment加锁后再去统计。

3.6 transfer

//这个类是一个标志,用来代表当前桶(数组中的某个下标位置)的元素已经全部迁移完成
static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        //把当前桶的头结点的 hash 值设置为 -1,表明已经迁移完成,
        //这个节点中并不存储有效的数据
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }
}

//迁移数据
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    //根据当前CPU核心数,确定每次推进的步长,最小值为16.(为了方便我们以2为例)
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    //从 addCount 方法,只会有一个线程跳转到这里,初始化新数组
    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 指代新数组
        nextTable = nextTab;
        //这里就把推进的下标值初始化为原数组长度(以16为例)
        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
    //i 代表当前线程正在迁移的桶的下标,bound代表它本次可以迁移的范围下限
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        //需要向前推进
        while (advance) {
            int nextIndex, nextBound;
            //(1) 先看 (3) 。i每次自减 1,直到 bound。若超过bound范围,或者finishing标志为true,则不用向前推进。
            //若未全部完成迁移,且 i 并未走到 bound,则跳转到 (7),处理当前桶的元素迁移。
            if (--i >= bound || finishing)
                advance = false;
            //(2) 每次执行,都会把 transferIndex 最新的值同步给 nextIndex
            //若 transferIndex小于等于0,则说明原数组中的每个桶位置,都有线程在处理迁移了,
            //于是,需要跳出while循环,并把 i设为 -1,以跳转到④判断在处理的线程是否已经全部完成。
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            //(3) 第一个线程会先走到这里,确定它的数据迁移范围。(2)处会更新 nextIndex为 transferIndex 的最新值
            //因此第一次 nextIndex=n=16,nextBound代表当次迁移的数据范围下限,减去步长即可,
            //所以,第一次时,nextIndex=16,nextBound=16-2=14。后续,每次都会间隔一个步长。
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                //bound代表当次数据迁移下限
                bound = nextBound;
                //第一次的i为15,因为长度16的数组,最后一个元素的下标为15
                i = nextIndex - 1;
                //表明不需要向前推进,只有当把当前范围内的数据全部迁移完成后,才可以向前推进
                advance = false;
            }
        }
        //(4)
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            //若全部线程迁移完成
            if (finishing) {
                nextTable = null;
                //更新table为新表
                table = nextTab;
                //扩容阈值改为原来数组长度的 3/2 ,即新长度的 3/4,也就是新数组长度的0.75倍
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            //到这,说明当前线程已经完成了自己的所有迁移(无论参与了几次迁移),
            //则把 sc 减1,表明参与扩容的线程数减少 1。
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                //在 addCount 方法最后,我们强调,迁移开始时,会设置 sc=(rs << RESIZE_STAMP_SHIFT) + 2
                //每当有一个线程参与迁移,sc 就会加 1,每当有一个线程完成迁移,sc 就会减 1。
                //因此,这里就是去校验当前 sc 是否和初始值是否相等。相等,则说明全部线程迁移完成。
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                //只有此处,才会把finishing 设置为true。
                finishing = advance = true;
                //这里非常有意思,会把 i 从 -1 修改为16,
                //目的就是,让 i 再从后向前扫描一遍数组,检查是否所有的桶都已被迁移完成,参看 (6)
                i = n; // recheck before commit
            }
        }
        //(5) 若i的位置元素为空,则说明当前桶的元素已经被迁移完成,就把头结点设置为fwd标志。
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        //(6) 若当前桶的头结点是 ForwardingNode ,说明迁移完成,则向前推进 
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        //(7) 处理当前桶的数据迁移。
        else {
            synchronized (f) {  //给头结点加锁
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    //若hash值大于等于0,则说明是普通链表节点
                    if (fh >= 0) {
                        int runBit = fh & n;
                        //这里是 1.7 的 CHM 的 rehash 方法和 1.8 HashMap的 resize 方法的结合体。
                        //会分成两条链表,一条链表和原来的下标相同,另一条链表是原来的下标加数组长度的位置
                        //然后找到 lastRun 节点,从它到尾结点整体迁移。
                        //lastRun前边的节点则单个迁移,但是需要注意的是,这里是头插法。
                        //另外还有一点和1.7不同,1.7 lastRun前边的节点是复制过去的,而这里是直接迁移的,没有复制操作。
                        //所以,最后会有两条链表,一条链表从 lastRun到尾结点是正序的,而lastRun之前的元素是倒序的,
                        //另外一条链表,从头结点开始就是倒叙的。看下图。
                        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);
                        }
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    //树节点
                    else if (f instanceof TreeBin) {
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                        (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                        (lc != 0) ? new TreeBin<K,V>(hi) : t;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}

4. 参考

我就知道面试官接下来要问我 ConcurrentHashMap 底层原理了

ConcurrentHashMap 原理解析(JDK1.8)