面试必问:JDK7 超详细ConcurrentHashMap源码解析

文章目录

        • 继承结构
        • 数据结构
        • 基本属性
        • 构造函数
        • 常用方法
          • put
          • scanAndLockForPut
          • remove
          • get
          • size
          • ContainsValue
        • 增长方式(扩容)
        • 设计思想

      ConcurrentHashMap是JUC下的类,可以认为是并发的HashMap,HashMap的升级版,关于底层是如何实现线程安全的,今天做一个解析。

继承结构

在这里插入图片描述
继承了AbstractMap,实现了ConcurrentMap接口和Serializable序列化接口。

数据结构

      说这个之前,先来说说HashTable,HashTable也是线程安全的集合,实现原理是HashTable对方法实现synchronize重量级锁,锁住了整张表,这样的话所有的操作都是串行化的,效率非常低。
      ConcurrentHashMap的底层数据结构是:数组+数组+链表
      正是得益于这种数据结构,可以实现高并发的功能。ConcurrentHashMap为了解决这个问题,对锁粒度进行了优化,将整个表进行了分段,即分段锁技术,将整张表分成了多个数组(Segment),然后每个数组元素又是一个HashMap(hash表),当需要并发时,锁住的是每个Segment,其他Segment还是可以操作的,这样不同Segment之间就可以实现并发,大大提高效率

ConcurrentHashMap:
面试必问:JDK7 超详细ConcurrentHashMap源码解析_第1张图片
      上面的Segment数组是ConcurrentHashMap的一个属性,数组名是segments,这就是将整张表分段处理的关键,segments中的每一个元素之间都是可以并发的,所以segments数组长度是多少,也就说明当前ConcurrentHashMap最多可以支持多少个并发级别。

Segment是一个内部类,看一下Segment的属性
Segment:
面试必问:JDK7 超详细ConcurrentHashMap源码解析_第2张图片
上面是Segment的所有概述,c表示继承结构,绿色的菱形表示属性,蓝色表示方法,我们主要看其中的table属性,如下图
面试必问:JDK7 超详细ConcurrentHashMap源码解析_第3张图片
      从上图可以看到table是一个HashEntry数组,HashEntry是真正存储数据的节点,可以类比HashMap中的Entry属性,可以理解为这里的一个Segment就是一个HashMap(可以这样理解,但不完全是这样),并且这里的table属性使用了volatile修饰,对所有线程可见

下面来看看HashEntry
面试必问:JDK7 超详细ConcurrentHashMap源码解析_第4张图片
      HashEntry同样是一个内部类,从上图的总体结构和属性名大概都可以知道这是一个怎样的类,HashEntry是一个链表的节点,具体来看看:
面试必问:JDK7 超详细ConcurrentHashMap源码解析_第5张图片
hash表示节点的hash值,这里的hash有两个用处,一个是决定了该节点属于哪个Segment,另外一个是决定属于table中的哪个链表

key表示存储键值对中的键

value表示键值对中的值,可以看到value用volatile修饰,这块就开始考虑多线程的修改了,表示对所有的线程都是可见的,这也是保证并发的必要条件。

next表示链表的下一个节点,因为每一个table都是数组加链表的结构,所以为了解决hash冲突,用拉链法,让同一映射到该位置的多个元素以链表的形式串起来。

现在就理解了 数组+数组+链表 这种数据结构在源码中是如何实现的了,画一张图来帮助理解
面试必问:JDK7 超详细ConcurrentHashMap源码解析_第6张图片

基本属性

	/**
     * The default initial capacity for this table,
     * used when not otherwise specified in a constructor.
     */
    //默认初始容量:16,这里指的是table数组的默认大小
	static final int DEFAULT_INITIAL_CAPACITY = 16;

    /**
     * The default load factor for this table, used when not
     * otherwise specified in a constructor.
     */
    //默认加载因子,可以类比HashMap中的加载因子,用于扩容,因为segments数组是
    //用来并发的,一旦确定就不能扩容,所以这个值会传给每个Segment,Segment对象
    //对table数组进行扩容。这个属性代表table数组中已经用的占比标准,默认为0.75,
    //如果table数组中非null占比大于0.75,就该扩容了。
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
   /**
     * The default concurrency level for this table, used when not
     * otherwise specified in a constructor.
     */
    //默认并发级别,代表可以同时并发的最大数,也就是segments数组的容量的大小
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    /**
     * The maximum capacity, used if a higher value is implicitly
     * specified by either of the constructors with arguments.  MUST
     * be a power of two <= 1<<30 to ensure that entries are indexable
     * using ints.
     */
    //最大容量,ConcurrentHashMap的最大容量,最大扩容为2^30,到这就不再扩容
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The minimum capacity for per-segment tables.  Must be a power
     * of two, at least two to avoid immediate resizing on next use
     * after lazy construction.
     */
    // 最小容量,table数组的最小容量:2
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

    /**
     * The maximum number of segments to allow; used to bound
     * constructor arguments. Must be power of two less than 1 << 24.
     */
    //最大segments容量,也就是说最大并发量为2^16即65536
    static final int MAX_SEGMENTS = 1 << 16; // slightly conservative

    /**
     * Number of unsynchronized retries in size and containsValue
     * methods before resorting to locking. This is used to avoid
     * unbounded retries if tables undergo continuous modification
     * which would make it impossible to obtain an accurate result.
     */
     //该变量在size方法和containsValue方法中用到,表示尝试锁的次数
    static final int RETRIES_BEFORE_LOCK = 2;

构造函数

ConcurrentHashMap的构造函数

	public ConcurrentHashMap(int initialCapacity,
	                         float loadFactor, int concurrencyLevel) {
	    //防御性检查
	    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
	        throw new IllegalArgumentException();
	    //传参传入的并发级别(segments数组的最大值)最大不能超过上面定义的常量,也就是2^16
	    if (concurrencyLevel > MAX_SEGMENTS)
	        concurrencyLevel = MAX_SEGMENTS;
	        
	    //sshift是ssize左移的次数,ssize是大于concurrencylevel的最小的2的整数倍
	    int sshift = 0;
	    int ssize = 1;
	    while (ssize < concurrencyLevel) {
	        ++sshift;
	        ssize <<= 1;
	    }
	    
	    //segmentshift和segmentMask用来定位节点属于segments数组中哪个元素,也就是定位到table
	    this.segmentShift = 32 - sshift;
	    this.segmentMask = ssize - 1;
	    
	    //如果传入的初始容量大于最大容量,则赋值为最大容量
	    if (initialCapacity > MAXIMUM_CAPACITY)
	        initialCapacity = MAXIMUM_CAPACITY;
	    //c为每张table数组的大小,这里获得c采用的是进一法,不是去尾法,体现在if,++c中。
	    int c = initialCapacity / ssize;
	    if (c * ssize < initialCapacity)
	        ++c;
	    //重新确定table数组的大小为cap,值为大于c的最小的2的整数倍
	    int cap = MIN_SEGMENT_TABLE_CAPACITY;
	    while (cap < c)
	        cap <<= 1;
	
	    //初始化s0,其实就是segments[0],传入加载因子,阈值和新创建的HashEntry数组(即table数组)
	    Segment<K,V> s0 =
	        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
	                         (HashEntry<K,V>[])new HashEntry[cap]);
	    //创建segments数组并初始化并发量的大小ssize
	    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
	    //安全的将segment0赋值到segments[0]
	    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
	    this.segments = ss;
	}

上面是最重要的构造函数,大体意思通过注释已经表现出来了

      首先会对传入的参数进行防御性检查,然后对传入的参数进行了一些处理,这些处理包括concurrencyLevel会变成大于它的最小的2的倍数,通过这些变量计算了segmentShift ,segmentMask ,这两个用来确定一个节点是哪个Segment,确定table数组的大小cap,把cap,loadFactor,计算的阈值传入构造创建一个Segment,创建ss数组,把ss数组的第1项初始化为刚才创建的Segment对象。

这里为什么是第一项呢?
其实是在put过程中进行用到进行初始化,关于这种思想以及深究在文末给出了详细剖析

点我到文末

其他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);
    }

      剩下的这几种都是第一种构造函数的重载,根据用户传入参数的不同稍微做了修改,如果用户没有传入哪些参数,则使用默认参数,最后调用第一种构造函数进行初始化。

Segment的构造函数

   Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
        this.loadFactor = lf;
        this.threshold = threshold;
        this.table = tab;
    }

可以看到Segment的构造函数非常简单,就是初始化加载因子,阈值和table数组。

常用方法

put

调用put方法时,首先会调用ConcurrentHashMap的put方法,先来看一下:

  public V put(K key, V value) {
      Segment<K,V> s;
      //值不能为null
      if (value == null)
          throw new NullPointerException();
      //hash函数,调用hash算法,返回一个让节点分布更为均匀的hash值
      int hash = hash(key);
      //通过segmentShift,segmentMask和hash值确定Segment,也就是在segments中定位元素
      int j = (hash >>> segmentShift) & segmentMask;
      //如果获取到的Segment为空,那么进入函数ensureSegment创建一个Segment
      if ((s = (Segment<K,V>)UNSAFE.getObject          
           (segments, (j << SSHIFT) + SBASE)) == null) 
          s = ensureSegment(j);
      //确定好Segment,代理给Segment进行put操作
      return s.put(key, hash, value, false);
  }

重申注释中需要注意的几点:

  1. 值不能为null,如果为空,就抛异常,当然key也不能为空,如果为空,在hash(key)这一步操作,会报空指针异常
  2. 最主要的是确定好是哪一个Segment,然后将主要put逻辑代理给了Segment内部的put进行实现

紧接着我们来看Segment中的put操作

final V put(K key, int hash, V value, boolean onlyIfAbsent) {        
			//trylock如果成功则获取到锁
			//不成功则调用scanAndLockForPut方法
			//scanAndLockForPut仍然会继续的trylock,lock,确保这里要获取到锁,方法详见往下看
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                //定位具体在数组中哪个链表,还是用hash定位
                int index = (tab.length - 1) & hash;
                //得到该链表的头结点
                HashEntry<K,V> first = entryAt(tab, index);
                //遍历链表
                for (HashEntry<K,V> e = first;;) {
                	//如果不为空,则比对,如果key相同,hash相同,说明已经有了该key
                	//将该节点的value修改为新的value,返回旧的oldValue、
                	//如果不相同,链表指针往后移动,遍历下一个节点
                    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;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    //如果为空,则证明已经到了链表的末尾,到了末尾还没找到,则证明没有该key
                    //创建一个新的节点存储,存储完成后容量加1判断一下是否需要扩容
                    //如果需要扩容,则需要重哈希(rehash)
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                        	//扩容,扩容在后面详细讲解
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
            	//执行完成,释放锁
                unlock();
            }
            return oldValue;
        }

代码的大体意思看注释,下面说一说重要的几点:

  1. 首先Segment的put操作是线程安全的,这个方法安全的机制是可重入锁(ReentrantLock),由于Segment这个内部类是继承了ReentranLock,所以在put操作中直调用了lock,trylock,unlock方法,同步监视器对象(锁住的对象)是方法的调用者,也就是当前Segment对象。
  2. 方法的第一步操作,尝试的加锁,如果不成功,调用scanAndLockForPut这个函数,为的就是确保能加上锁,换句话说,不管怎么样,执行完这个三目运算符,定会对当前Segment对象加锁
  3. put的大体流程和思路都可以类比HashMap,无非就是对数据的特点进行约束,可以为空了什么的,这点在ConcurrentHashMap的put中进行了控制,下来就是判断是否已经有了该key值的节点,有的话就进行替换value,没有的话就创建一个新的节点插入进去,插入后去判断是否扩容,扩容最重要的一步便是重哈希,这些操作在Segment的put操作中进行了体现。

上述的put过程用一副流程图来描述:
                  面试必问:JDK7 超详细ConcurrentHashMap源码解析_第7张图片

下面看一下能确保加锁的函数scanAndLockForPut:

scanAndLockForPut
	private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
			//根据this(Segment)和hash确定table中的索引,拿到key所在链表的第一个节点
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            //retries表示尝试获取锁的次数
            int retries = -1; 
            //获取锁失败时一直循环,除非tryLock成功或者达到自旋次数,直接Lock,退出
            while (!tryLock()) {
                HashEntry<K,V> f; // to recheck first below
                //第一阶段(retries < 0):没有找到key相同的节点或者没有遍历完该链表
                if (retries < 0) {
                	//如果当前索引链表为空,或者循环到链表的最后
                    if (e == null) {
                    	//判断node是否被初始化过,如果没有则初始化,置retries为0,进入第二阶段
                        if (node == null)
                            node = new HashEntry<K,V>(hash, key, value, null);
                        retries = 0;
                    }
                    //key匹配上了,所以就找到了节点,置retries为0,进入第二阶段
                    else if (key.equals(e.key))
                        retries = 0;
                    //指针移动,往后遍历
                    else
                        e = e.next;
                }
                //第二阶段(retries >= 0):如果尝试次数大于最大获取锁的次数,则进行Lock操作,退出循环
                //MAX_SCAN_RETRIES取决于cpu,在下面有讲解
                else if (++retries > MAX_SCAN_RETRIES) {
                	//lock操作,获取不到锁就阻塞,直到获取到锁跳出循环
                    lock();
                    break;
                }
                //第二阶段(retries >= 0):因为插入元素是头插法,所以这块是为了判断首个预期值是否等于现在的值
                //如果不等于,则证明其他线程已经插入了元素,retries=-1,进入第一阶段重新开始
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
            return node;
        }

      可以从代码的注释中知道,我把这个函数分为两个阶段,第一个阶段为retries<0,第二个阶段为retries>=0,在代码的第一阶段,就是遍历该条链表该key是否已经存在,如果存在进入第二阶段,最后返回null,覆盖新值交给外部的put去做,如果不存在就创建该节点进行返回,所以第一阶段就是创建这个新节点
      第二阶段分为两个步骤,首先去判断自旋次数是否已经超过标准,如果超过了标准就直接Lock,退出循环,如果没有超过标准,就继续让它自旋,不过在下一次自旋之前需要先判断线程安全的问题,即是否有其他线程修改过这一条链表,如果修改过,那么直接进入第一阶段(因为有可能其他线程插入了一个key相同的节点),一切重新开始。
整个过程可以用一张图来表示:
面试必问:JDK7 超详细ConcurrentHashMap源码解析_第8张图片

来看看这个最大值,静态变量MAX_SCAN_RETRIES

		//cpu的核数
        static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

      判断cpu的可用核数,如果大于1,那么trylock64次才lock,如果不是,则trylock一次就去lock。
      trylock和lock的区别就是,trylock只是尝试的去获取锁,获取不到就返回,lock是如果不获取到锁就阻塞当前线程。
      所以结合上面的过程就可以知道自旋获取锁只会在cpu比较空闲的情况下才会进行,如果cpu利用率比较高,那么就阻塞掉该线程,等待锁。这就是一个优化。

remove

同样,remove方法也是代理给了Segment进行实现,来分析一下源码

        final V remove(Object key, int hash, Object value) {
        	//如果尝试获取锁失败,就调用方法确保获取到锁
            if (!tryLock())
                scanAndLock(key, hash);
            V oldValue = null;
            try {
            	//拿到table数组,算出位置,通过这两个信息拿到对应链表的第一个结点
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> e = entryAt(tab, index);
                //定义删除节点的前一个节点
                HashEntry<K,V> pred = null;
                //遍历链表,当没有遍历完的时候
                while (e != null) {
                    K k;
                    //记录要删除节点的下一个节点
                    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)
                            	//前一个节点为空,则删除的是第一个节点,直接把下一个节点设为链表头
                                setEntryAt(tab, index, next);
                            else
                            	//前一个节点不为空,那么把前一个节点的next设为下一个节点
                                pred.setNext(next);
                            ++modCount;
                            --count;
                            oldValue = v;
                        }
                        break;
                    }
                    pred = e;
                    e = next;
                }
            } finally {
                unlock();
            }
            return oldValue;
        }
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);
        //定位键为key的对象在segments数组中的位置,也就是确定Segment
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
            //for循环初始化部分就进行定位HashEntry在table中的位置,然后遍历链表,寻找
            //节点,找到后返回对应的value,找不到返回null
            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是没有加锁的,那么如何保证get操作的安全性呢?
      首先,get是一个读的过程,读的过程并不会修改数据,所以读和读也就是get和get之间是线程安全的。
但是同时读和写就会产生不同,如果一个线程在put,一个线程在get,如何保证线程安全

  1. 如果put在get之后,get操作已经遍历到链表的中间,这时候put操作如果操作的是一个已经存在的key值,那么put过后,get是可以立即可见的,为什么呢,可以看看Segment类的Value属性是有volatile修饰的,所以可以保证对其他线程的可见性。如果put操作操作的是一个不存在的key,这个时候,get是读不到数据的,但是同样是线程安全的,只不过需要再次的去读才能读到。
  2. 如果put在get之前,这样就非常好处理了,因为Segment中的table属性就是volatile修饰的,这时只需要在查找前,获取一下最新状态的的table就可以了,如果修改了就会通过UNSAFE.getObjectVolatile(segments, u)更新table,这样查找的就是正确的结果。
size
    public int size() {
        // Try a few times to get accurate count. On failure due to
        // continuous async changes in table, resort to locking.
        final Segment<K,V>[] segments = this.segments;
        int size;
        boolean overflow; // true if size overflows 32 bits
        long sum;         // sum of modCounts
        long last = 0L;   // previous sum
        int retries = -1; // first iteration isn't retry
        try {
            for (;;) {
                if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                    	//锁住所有Segment,也就是锁住了segments数组
                        ensureSegment(j).lock(); // force creation
                }
                sum = 0L;
                size = 0;
                overflow = false;
                for (int j = 0; j < segments.length; ++j) {
                    Segment<K,V> seg = segmentAt(segments, j);
                    if (seg != null) {
                        sum += seg.modCount;
                        int c = seg.count;
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                if (sum == last)
                    break;
                last = 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;
    }

      size方法是对segments中所有table的统计,所以是需要对所有Segment对象加锁,加锁后再进行统计,该方法相对于HashMap效率是比较低的。

ContainsValue
public boolean containsValue(Object value) {
        // Same idea as size()
        if (value == null)
            throw new NullPointerException();
        final Segment<K,V>[] segments = this.segments;
        boolean found = false;
        long last = 0;
        int retries = -1;
        try {
        	//对所有Segment加锁
            outer: for (;;) {
                if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }
                long hashSum = 0L;
                int sum = 0;
                for (int j = 0; j < segments.length; ++j) {
                    HashEntry<K,V>[] tab;
                    Segment<K,V> seg = segmentAt(segments, j);
                    if (seg != null && (tab = seg.table) != null) {
                        for (int i = 0 ; i < tab.length; i++) {
                            HashEntry<K,V> e;
                            for (e = entryAt(tab, i); e != null; e = e.next) {
                                V v = e.value;
                                if (v != null && value.equals(v)) {
                                    found = true;
                                    break outer;
                                }
                            }
                        }
                        sum += seg.modCount;
                    }
                }
                if (retries > 0 && sum == last)
                    break;
                last = sum;
            }
        } finally {
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        return found;
    }

这样做主要是为了避免在统计或者查找过程中,其他线程在其他Segment(分段锁)进行了操作。

增长方式(扩容)

刚才在put中提到了扩容,整个过程也就是rehash函数,下面看看这个函数

        private void rehash(HashEntry<K,V> node) {
            HashEntry<K,V>[] oldTable = table;
            int oldCapacity = oldTable.length;
            //2倍扩容
            int newCapacity = oldCapacity << 1;
            //计算阈值
            threshold = (int)(newCapacity * loadFactor);
            //创建新的table
            HashEntry<K,V>[] newTable =
                (HashEntry<K,V>[]) new HashEntry[newCapacity];
            //该值用于后面的hash运算与,保证运算后的值落到数组的范围内
            int sizeMask = newCapacity - 1;
            //遍历旧表中的每一个头结点
            for (int i = 0; i < oldCapacity ; i++) {
                HashEntry<K,V> e = oldTable[i];
                //判断该位置头结点是否为空,为空就没必要遍历该条链表
                if (e != null) {
                	//第一个数据节点
                    HashEntry<K,V> next = e.next;
                    //e节点的新的位置,保证idx落到数组的范围内,这块其实相当于重新hash,sizeMask是newCapacity得来的
                    int idx = e.hash & sizeMask;
                    //如果只有一个数据节点,那么直接挪到相应的位置
                    if (next == null)   //  Single node on list
                        newTable[idx] = e;
                    else { // Reuse consecutive sequence at same slot
                    	//该条链表rehash后,最后几个节点保持一致的分界点,下面画图举例解释,请细看下面第三点
                        HashEntry<K,V> lastRun = e;
                        //记录lastRun节点的位置
                        int lastIdx = idx;
                        for (HashEntry<K,V> last = next;
                             last != null;
                             last = last.next) {
                            int k = last.hash & sizeMask;
                            //如果k=lastIdx,就不更新lastIdx和lastRun
                            if (k != lastIdx) {
                                lastIdx = k;
                                lastRun = last;
                            }
                        }
                        //将lastRun后面所有链表一同挪过去
                        newTable[lastIdx] = lastRun;
                        // Clone remaining nodes
                        //从头到lastRun一个一个的重新hash,放入该放的位置,方式为头插
                        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];
                            newTable[k] = new 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;
        }
  1. 扩容首先需要注意的是对Segment中的table进行的扩容,不是对ConcurrentHashMap的segments
  2. 扩容是2倍扩容
  3. 放到第4点的前面,是第四点的基础,需要了解的是重hash得到的值无非是两个值,一个是原位置,一个是原位置加上一个扩容大小的值,举个例子来理解:
    原本在oldtable中3号位置的链表节点,说说这两种:0000 0011 和 0001 0011
    oldtable的大小假定为16,那么这两个&完15(0000 1111)后,都是0000 0011,所以这两个是3号位置
    而扩容后,做&是(32-1)=31=0001 1111,这两个数字&完是不同的位置,一个是3号位置,一个是19号位置,差了扩容的大小16个位置。
    下面来看lastRun到底是干什么的?在lastRun后面有一个for循环,遍历了整个链表,整个链表其实重哈希后只有两个位置,下图我用红色和绿色来代替,那么从链表头开始,一旦颜色发生变化lastRun就更新到改变颜色的节点,从下图这个例子来看:
    面试必问:JDK7 超详细ConcurrentHashMap源码解析_第9张图片
    再举几个例子帮助理解:
    面试必问:JDK7 超详细ConcurrentHashMap源码解析_第10张图片
    面试必问:JDK7 超详细ConcurrentHashMap源码解析_第11张图片
    这下应该理解lastRun了,这样做就是为了把lastRun后面的所有节点一次性挪过去。
  4. 不同于HashMap的地方,这个扩容比较复杂,其中用了两个并列的for循环,把一个链表分成了两部分
    如果不要第一个for循环,似乎把所有的节点一个一个重新进行hash,也是完全可以实现的,但是有了第一个for循环,他就会从后往前开始进行计算,把从后往前算都是一样位置的节点都放在一块,分界点就是lastRun节点,lastRun后面的节点都在lastIdx位置,所以可以一次性的把这些节点挪过去,第二个for就是从头结点到lastrun一个一个进行计算,计算完后以头插的方式一个一个挪动。

设计思想

1.关于为什么初始化的时候只初始化了一个segment,其他都为null,在哪里进行了初始化呢?

      这是一种设计思想:为了尽可能的节省空间,只有在用到的时候才会进行初始化,分配空间。在jdk中很多地方都用到了这种思想。

首先我们来看一下ConcurrentHashMap的构造函数
面试必问:JDK7 超详细ConcurrentHashMap源码解析_第12张图片
框中有三句话

  1. 创建一个Segment对象s0
  2. 创建Segment数组ss
  3. 原子化的将ss数组中的0号元素赋值为s0

      可以明显的看到问题,这里只初始化了第一个数组元素,后面的数组元素都是null,只有在put操作的时候,用到才会进行初始化,那么不禁有一个问题,那为什么要创建这第一个数组元素呢,一个都不创建用的时候再创建不是更符合上面的设计思想吗?

      这是因为在ConcurrentHashMap的构造函数中,已经可以确定每一个Segment中的加载因子,阈值和table数组的开辟大小,所以利用这些值就先创建一个作为例子,后续在创建其他Segment对象时,就根据这第一个标准进行创建,具体来看一下代码来理解这段话:

ConcurrentHashMap的put方法:
面试必问:JDK7 超详细ConcurrentHashMap源码解析_第13张图片
      可以看到框中所表达的意思:如果根据hash原子的获得对应位置的Segment为null,则会调用下面的那个函数,来看一下ensureSegment函数
面试必问:JDK7 超详细ConcurrentHashMap源码解析_第14张图片
      和我们分析的一样,取出了第一个Segment数组元素,拿到该元素的加载因子,阈值,还有table数组的长度,用这三个值初始化了Segment对象进行了返回。

所以,除过第一个Segment数组元素,其他的都是在这里进行初始化的。
回到刚才位置

你可能感兴趣的:(Java高并发)