JDK源码系列 ConcurrentHashMap源码分析

ConcurrentHashMap

之前我们已经分析完HashMap的源码,也知道了HashMap内部的相关运行机制,可是HashMap本身并不是一个线程安全的容器类。那有线程安全的HashMap吗? 当然有,HashTable就是线程安全的,但是它十分低效。当然,JUC包有提供一个线程安全且高效的HashMap实现,那就是ConcurrentHashMap

本文就针对ConcurrentHashMap的实现原理进行分析,基于JDK1.7版本。

ConcurrentHashMap源码分析

ConcurrentHashMap内部大致结构
在这里插入图片描述

ConcurrentHashMap采用的是“分段锁”的策略,ConcurrentHashMap的底层结构就是一个Segment数组。

final Segment<K,V>[] segments

我们来看一下Segment类:

1. Segment类
static final class Segment<K,V> extends ReentrantLock implements Serializable

我们可以看到Segment类继承ReentrantLock类,至于ReentrantLock,可以看一下JDK源码系列 ReentrantLock 公平锁和非公平锁的实现原理,其实它和Synchronized关键字类似,都是可重入锁,但是功能比Synchronized关键字强大。
既然Segment继承了ReentrantLock,那么说明Segment本身就是一个锁。

Segment类似一个HashMap,一个Segment维护着一个HashEntry数组。

transient volatile HashEntry<K,V>[] table;

HashEntry类是我们真正用来存储的key-value键值对的数据结构,也是ConcurrentHashMap的最小逻辑存储单元。一个ConcurrentHashMap内部维护着一个Segment数组,每个Segment各自维护着一个HashEntry数组。

HashEntry实现

/*HashEntry和Hash的Node节点基本一模一样 都是一个链表结构*/
static final class HashEntry<K,V> {
    final int hash; //存储hash值
    final K key; 
    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;
    }
    /*设置下一个节点*/
    final void setNext(HashEntry<K,V> n) {
        UNSAFE.putOrderedObject(this, nextOffset, n);
    }
    /*Unsafe*/
    static final sun.misc.Unsafe UNSAFE;
    /*next节点的地址偏移量*/
    static final long nextOffset;
 	/*静态块 通过UnSafe获取当前节点的下一个节点的地址偏移量*/
    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和我们之前分析的JDK8的HashMap的内部类Node几乎一模一样。
不过这里出现了点新东西UnSafe类,其实也不算新东西,这个类在JUC包中十分常见。有兴趣可以自己去了解一下,这个类非常重要,这里不赘述。

1.1 Segment参数分析:
/*存储key-value键值对*/
transient volatile HashEntry<K,V>[] table;
/*Segment内存存储元素的数量*/
transient int count;
/*fast-fail操作 遍历该Segment时 若其他线程对该Segment进行更改操作 抛出异常*/   
transient int modCount; 
/*阈值*/ 
transient int threshold;
/*负载因子*/
final float loadFactor;
/*scanAndLockForPut方法tryLock的最大次数 单核1 多核64*/
static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

这些参数和HashMap的参数几乎一样,就不做过多的讲解。

1.2 Segment构造器
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
    this.loadFactor = lf; //设置负载因子
    this.threshold = threshold; //设置阈值
    this.table = tab; //设置该Segment的table数组
}
1.3 Segment的核心API

put(K key, int hash, V value, boolean onlyIfAbsent)

/*onlyIfAbsent为true的话,若该key原本就存在table中 那么value不进行更新*/
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    //tryLock是ReentrantLock的方法 尝试获取锁
    //在进行写入过程成 先获取当前Segment的独占锁
    //若获取锁成功 则node设置为null 若没有获取到锁 则采用scanAndLockForPut方法
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    //旧的值
    V oldValue;
    try {
    	/*获取Segment的内部数组*/
        HashEntry<K,V>[] tab = table;
        /*获取key值在table中的位置*/
        int index = (tab.length - 1) & hash;
        /*获取桶的第一个节点*/
        HashEntry<K,V> first = entryAt(tab, index);
        /*遍历桶*/
        for (HashEntry<K,V> e = first;;) {
        	/*如果当前节点不为空*/
            if (e != null) {
                K k;
                /*进行一致性判断*/
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    /*若命中 获取旧值*/
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                    	//修改值
                        e.value = value;
                        //modCount加一 用于快速失败
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
            	/*如果node不为空 即tryLock时没有获取独占锁 设置node的下一个节点为first*/
                if (node != null)
                    node.setNext(first);
                else
                	//若node为空 直接新建一个节点 插入当前链表的头部
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                /*若当前table容纳的元素个数超过阈值 则进行扩容*/
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                	/*若没有达到阈值 将node链表插入当前桶*/
                    setEntryAt(tab, index, node);
                ++modCount; //fast-fail
                count = c;
                oldValue = null; //help GC
                break;
            }
        }
    } finally {
        unlock(); //解锁
    }
    return oldValue;
}

整个put的流程梳理一下:

  1. 先通过tryLock尝试获取当前Segment的独占锁,获取失败的话,调用scanAndLockForPut(key, hash, value)方法。成功则进入下一步。
  2. 根据key的hash值获取它在当前哈希表中桶的位置。
  3. 对桶进行遍历,若找到匹配key,根据onlyIfAbsent的值判断是否对value进行更新,操作完,退出循环。
  4. 若没有找到匹配的key,新建一个Entry节点。
    - 若当前table存储元素个数超过阈值,进行扩容操作。
    - 若没有超过阈值,则将新建节点设置为当前桶的头结点。

我们来分析一下在put中出现的函数:

scanAndLockForPut(key, hash, value) : 当put方法快速尝试获取独占锁失败时,会进入该方法。

private ConcurrentHashMap.HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    /*通过Segment和hash值获取当前key值在table中的位置 并返回桶的第一个结点*/
    ConcurrentHashMap.HashEntry<K,V> first = entryForHash(this, hash);
    ConcurrentHashMap.HashEntry<K,V> e = first;
    ConcurrentHashMap.HashEntry<K,V> node = null;
    /*retries为重试次数*/
    int retries = -1; // negative while locating node
    /*循环 尝试获取锁 直至获取锁成功*/
    while (!tryLock()) {
        ConcurrentHashMap.HashEntry<K,V> f; // to recheck first below
        if (retries < 0) {
            /*若第一个节点为空*/
            if (e == null) {
                if (node == null)
                	/*这里顺便初始化node节点*/
                    node = new ConcurrentHashMap.HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            /*若key值匹配*/
            else if (key.equals(e.key))
                retries = 0;
            else
            	/*遍历*/
                e = e.next;
        }
        /*若重试的次数超过MAX_SCAN_RETRIES 则直接使用lock()*/
        else if (++retries > MAX_SCAN_RETRIES) {
            lock();
            break;
        }
		/*当头结点由于并发原因有新的节点插入了链表成为新的表头 则重置头结点和retries 相当于重新进行一次scanAndLockForPut方法*/
        else if ((retries & 1) == 0 &&
                (f = entryForHash(this, hash)) != first) {
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

我们来理一下scanAndLockForPut的过程,它是通过循环tryLock获取锁的。这个过程中会增加retries的值,若大于MAX_SCAN_RETRIES的话(以免自旋获取锁时间过长占用过多资源),直接进行lock(),进入Sync队列中等待获取锁,lock()方法会阻塞当前线程,直至当前线程获取独占锁成功。(使用lock的话会让当前线程进入WAITING状态(不会占用CPU资源),直至被LockSupport.unpark()唤醒)
(retries & 1) == 0 &&(f = entryForHash(this, hash)) != first判断说明当前的Segment有其他线程进行put操作且已经释放了锁,那么当前线程有很大可能能重新获取锁,于是就重置retries和first的值。
其实scanAndLockForPut就做了一件事,使当前线程获取Segment的独占锁,顺带实例化一下node节点(可能执行也可能不执行 不执行是直接一次tryLock就获取了锁)。

rehash(node) 函数:
当执行put的时候,新建节点若使count超过阈值,则会执行该方法:

private void rehash(ConcurrentHashMap.HashEntry<K,V> node) {
    // 获取table
    ConcurrentHashMap.HashEntry<K,V>[] oldTable = table;
    // 旧的capacity
    int oldCapacity = oldTable.length;
    // 扩容一倍
    int newCapacity = oldCapacity << 1;
    // 新的阈值
    threshold = (int)(newCapacity * loadFactor);
    // 创建新的table
    ConcurrentHashMap.HashEntry<K,V>[] newTable =
            (ConcurrentHashMap.HashEntry<K,V>[]) new ConcurrentHashMap.HashEntry[newCapacity];
    int sizeMask = newCapacity - 1;
    for (int i = 0; i < oldCapacity ; i++) {
        ConcurrentHashMap.HashEntry<K,V> e = oldTable[i];
        if (e != null) {
            ConcurrentHashMap.HashEntry<K,V> next = e.next;
            //获取e在新table中的位置
            int idx = e.hash & sizeMask;
            /*若next结点为空*/
            if (next == null)   //  Single node on list
            	/*直接移动到新的Table*/
                newTable[idx] = e;
            else { 
            	//不为空 进行遍历 e的链表的表头
                ConcurrentHashMap.HashEntry<K,V> lastRun = e;
                int lastIdx = idx;
                //e的下一个结点开始遍历
                for (ConcurrentHashMap.HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }
                newTable[lastIdx] = lastRun;
              	/*再次遍历链表*/
                for (ConcurrentHashMap.HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    V v = p.value;
                    int h = p.hash;
                    int k = h & sizeMask;
                    ConcurrentHashMap.HashEntry<K,V> n = newTable[k];
                    /*采用头插法插入*/
                    newTable[k] = new ConcurrentHashMap.HashEntry<K,V>(h, p.key, v, n);
                }
            }
        }
    }
    /*将新的结点添加到table中*/
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}

我们来看一下这个for (ConcurrentHashMap.HashEntry<K,V> last = next; last != null; last = last.next)这个for循环做了什么。
在这里插入图片描述
其实就是找到lastRun这个位置,然后将后面的节点进行一个整体的搬迁。注意: 后面的节点高位全是1。
其实我还是觉得JDK8 HashMap那样采取loHead,hiHead比较好一点,只需要一次for循环。
因为该这个for循环可能会遍历到最后一个节点,那么此处for循环对下面的for循环并没有任何帮助。

其实rehash的过程和HashMap的resize过程一样的,只是细节有点不一样而已,而且这里容量为2的幂次的好处也不再赘述。具体可以看JDK源码系列 HashMap源码剖析

remove(Object key, int hash, Object value)

接下来我们来看下remove方法:

final V remove(Object key, int hash, Object value) {
    /*尝试获取独占锁*/
    if (!tryLock())
    	/*获取不到则进入scanAndLock(key, hash)方法获取锁*/
        scanAndLock(key, hash);
    V oldValue = null;
    try {
        ConcurrentHashMap.HashEntry<K,V>[] tab = table;
        /*获取key在table中的锁*/
        int index = (tab.length - 1) & hash;
        ConcurrentHashMap.HashEntry<K,V> e = entryAt(tab, index);
        /*记录前一个*/
        ConcurrentHashMap.HashEntry<K,V> pred = null;
        /*对桶进行遍历*/
        while (e != null) {
            K k;
            ConcurrentHashMap.HashEntry<K,V> next = e.next;
            //命中
            if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                V v = e.value;
                /*值匹配*/
                if (value == null || value == v || value.equals(v)) {
                    /*如果前驱节点为空*/
                    if (pred == null)
                    	/*直接设置next节点为表头*/
                        setEntryAt(tab, index, next);
                    else
                    	/*将前驱节点的后继指针指向当前节点的后继节点*/
                        pred.setNext(next);
                    ++modCount; //fast-fail
                    --count;
                    oldValue = v;
                }
                break;
            }
            pred = e;
            e = next;
        }
    } finally {
        unlock();
    }
    return oldValue;
}

remove操作十分简单:先进行一次快速获取锁,若失败,则进入scanAndLock(key, hash)方法尝试获取锁,该方法和上面的scanAndLockForPut几乎相同,都是尝试获取锁,直至成功。然后获取锁成功后,遍历桶,删除目标节点。

replace方法和remove方法相似,就不赘述了。

clear()

final void clear() {
    lock(); //加锁
    try {
    	//遍历哈希表
        ConcurrentHashMap.HashEntry<K,V>[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            setEntryAt(tab, i, null);
        ++modCount;
        count = 0;
    } finally {
        unlock();
    }
}

说个小细节: ReentrantLock不会自动释放锁,所以我们需要将解锁任务放在finally块中保证锁资源一定会释放。


以上就是Segment的全部内容,相比HashMap1.7版本就是多了一个锁的释放和锁的获取,以及采用UnSafe类通过地址偏移量进行值的相关操作。

2.ConcurrentHashMap相关API

分析完Segment相关的API,接下来我们来对ConcurrentHashMap进行分析。

2.1 ConcurrentHashMap参数
//Map默认的初始大小为16 一定是2的幂次
static final int DEFAULT_INITIAL_CAPACITY = 16;
//默认的负载因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//默认的并发级别
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//map的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//每个Segment的最小容量 必须是2的幂次
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
//最大的Segment的数量 必须是2的幂次
static final int MAX_SEGMENTS = 1 << 16; 
//这个参数在调用size()的时候会需要 即在进行加锁前重试的次数为2
static final int RETRIES_BEFORE_LOCK = 2;
//以下参数后面再重点讲解
final int segmentMask;
final int segmentShift;
2.2.ConcurrentHashMap构造器
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    //Map的最大并发等级为1<<16=65536,也就是最大并发数为65536
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    //2的sshif次方等于ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
    int sshift = 0;
    int ssize = 1;
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    //这两个变量用来定位Segment 下面再讲
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    //initialCapacity是整个Map的容量大小
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //这里根据initialCapacity计算每个Segment数组中可以分到的容量大小
    //例如initialCapacity为64, 那么16个Segment的话,每个容量为4
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    //cap的最小值是2 因为cap*0.75约为2,不会导致Segment插入一个数就马上扩容
    while (cap < c)
        cap <<= 1;
   	// 创建Segment数组 并创建数组的第一个元素Segment[0] 其余的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];
    //往数组写入segment[0]
    UNSAFE.putOrderedObject(ss, SBASE, s0); 
    this.segments = ss;
}

当我们使用构造函数构造Segment数组后,Segment数组的大小将不能再发生改变。
通过构造函数我们来将一下几个参数的具体作用:

DEFAULT_CONCURRENCY_LEVEL:Concurrent容器的并发级别,默认是16,说明同一时间可以有16个线程对HashMap进行更改操作。支持最大的并发量为1<<16(65536)。
关于segmentMask和segmentShift两个值的作用
segmentMask和segmentShift这两个全局变量的主要作用是用来定位Segment的。
int j=(hash>>>segmentShift)&segmentMask
segmentMask: 段掩码,假如segment数组长度为16,则段掩码为16-1=15;segment数组长度为32,则段掩码为32-1=31。这样子得到的二进制低位bit都是1,可以更好地保证散列的均匀性。(这个地方和根据key的hash值寻找在哈希表的位置其实是一样道理的(hash&len-1))。
segmentShift: 由上面代码可得构造器2的sshift幂次等于ssize,segmentShift=32-sshift。由上面代码可得,Segment数组的大小ssize是由concurrentLevel所决定的,而且ssize必定是2的幂次。
若Segment数组的长度为16,那么sshift为4。且计算出来的hashCode是32位的,那么segmentShift则为16-4=12,经过无符号左移segmentShift后,只保留了高4位(其余的位置不需要),然后与segmentShift进行位运算来定位到Segment。

小技巧:如果当前哈希表容量为16,那么会取高四位来进行Segment的定位。其实高几位我们完全可以由segmentMask来获取。假设容量是16,那么segmentMask-1=15,其二进制是0000 0000 0000 1111,则取hash的高4位。若容量是32,那么segmentMask-1=31,其二进制是0000 0000 0001 1111,取hash的高5位来定位。

2.3.ConcurrentHashMap的核心API

put(K key, V value)

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    //通过hash函数计算出当前的hash
    int hash = hash(key.hashCode());
    //通过segmentShift和segmentMask来定位Segment的位置
    int j = (hash >>> segmentShift) & segmentMask;
    //调用UnSafe类获取相应的Segment 若为空 调用ensureSegment
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    //否则调用Segment类的put函数
    return s.put(key, hash, value, false);
}

put函数的流程:

  1. 调用hash函数对key的hashCode进行再hash。
  2. 调用hash >>> segmentShift) & segmentMask获取Segment的定位。
  3. 如果定位的Segment还为初始化,调用ensureSegment(j)进行初始化。
  4. 调用Segment的put函数。

我们来看一下ensureSegment函数:

ensureSegment: ConcurrentHashMap初始化的时候会初始化第一个槽Segment[0]。对于其他槽来说,在插入第一个值的时候需要进行初始化。

private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    //获取该Segment的地址偏移量
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        //获取segment[0]
        Segment<K,V> proto = ss[0]; 
        //获取segment[0]的长度
        int cap = proto.table.length;
        //获取segment[0]的负载因子
        float lf = proto.loadFactor;
        //获得阈值
        int threshold = (int)(cap * lf);
        //重新检查segment[k]是否已经被其他线程初始化
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // recheck
            //新建一个segment
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            //再次判断segment[u]是否被初始化
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                //采用cas来进行初始化
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

ensureSegment的作用就是用来进行初始化Segment的,对于并发操作采用CAS来进行控制。


putIfAbsent(K key, V value): 作用是不存在即创建,若存在则不进行修改。

public V putIfAbsent(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key.hashCode());
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject
         (segments, (j << SSHIFT) + SBASE)) == null)
        s = ensureSegment(j);
    return s.put(key, hash, value, true);
}

代码和put基本一致,只是调用Segment的put函数时,只是了onlyIfAbsent为true,该值的作用之前有解释,不做赘述。


get(Object key): 获取指定key值的value

public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    int h = hash(key.hashCode());
    //对Segment进行定位
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    //若该槽已经初始化
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        //通过tab.length - 1) & h定位在table中的桶的位置 再进行遍历
        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;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

get函数执行流程:

  1. 计算 hash 值,找到 segment 数组中的具体位置定位。
  2. 找到当前key值在Segment的table数组中的具体位置。
  3. 遍历桶,存在则返回对应value,否则返回null。

其他API基本都是get的衍生,例如containsKeyreplace,这些就不做分析了。


我们着重来看一下size() 这个函数的源码:

public int size() {
    //获取Segment数组
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; //判断是否溢出
    long sum;         //计算modCount的总和
    long last = 0L;   // previous sum
    int retries = -1; // 重试的次数
    try {
        for (;;) {
        	//如果统计两次都发生modCount值增加的话 直接锁住整个Segment数组
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // force creation
            }
            sum = 0L;
            size = 0;
            overflow = false;
            //遍历Segment数组
            for (int j = 0; j < segments.length; ++j) {
            	//获取segment[j]
                Segment<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                	//加上modCount
                    sum += seg.modCount;
                    //获取segment[j]存储元素的个数
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            if (sum == last)
                break;
            last = sum; //记录上一次的sum
        }
    } finally {
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}

ConcurrentMap是采用分段锁实现的,那么当我们想统计所有整个ConcurrentHashMap所存储的容量时,可以调用size()函数来获取。

我们来看看是源码的实现size()的流程。

  1. 先遍历一次记录所有槽的modCount的总和sum。
  2. 再进行两次遍历,若统计过程中sum没有发生变化,说明其间没有线程对Segment数组进行更改操作,那么直接返回答案。
  3. 若两次遍历后,sum都发生变化,那么将锁住整个Segment数组进行统计。

以上便是,ConcurrentHashMap的全部源码讲解,若有错误的地方,请指出,感谢。

参考文章

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值