In-depth understanding of HashMap (jdk7)

Storage structure illustrated in the HashMap

jdk1.7 HashMap is implemented using an array + single chain, although the definition of the hash function to avoid conflicts, but because of the limited length of the array, or two different Key will be calculated after the same position in the array, using version 1.7 the list to resolve.

Can be found from the above simplified illustration, if located too many nodes in the linked list, it is clear the key value by sequentially lookup inefficient, it was carried out in 1.8 improved using an array list + red + black tree is achieved when the list is longer than the threshold 8, convert the list is red-black tree. specific details on the reference I a summary of the in-depth understanding of HashMap jdk8

From the above figure actually know each element Entry types, which look at the following attributes (renamed in 1.8 Entry Node, also achieved of Map.Entry) Entry there.

//hash标中的结点Node,实现了Map.Entry
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
	//Entry构造器,需要key的hash,key,value和next指向的结点
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    public final K getKey() { return key; }

    public final V getValue() { return value; }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    //equals方法
    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }
	//重写Object的hashCode
    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    public final String toString() {
        return getKey() + "=" + getValue();
    }

	//调用put(k,v)方法时候,如果key相同即Entry数组中的值会被覆盖,就会调用此方法。
    void recordAccess(HashMap<K,V> m) {
    }

    //只要从表中删除entry,就会调用此方法
    void recordRemoval(HashMap<K,V> m) {
    }
}
复制代码

HashMap of member variables and meaning

//默认初始化容量初始化=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量 = 1 << 30
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认加载因子.一般HashMap的扩容的临界点是当前HashMap的大小 > DEFAULT_LOAD_FACTOR * 
//DEFAULT_INITIAL_CAPACITY = 0.75F * 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//默认是空的table数组
static final Entry<?,?>[] EMPTY_TABLE = {};

//table[]默认也是上面给的EMPTY_TABLE空数组,所以在使用put的时候必须resize长度为2的幂次方值
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

//map中的实际元素个数 != table.length
transient int size;

//扩容阈值,当size大于等于其值,会执行resize操作
//一般情况下threshold=capacity*loadFactor
int threshold;

//hashTable的加载因子
final float loadFactor;

/**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
transient int modCount;

//hashSeed用于计算key的hash值,它与key的hashCode进行按位异或运算
//hashSeed是一个与实例相关的随机值,用于解决hash冲突
//如果为0则禁用备用哈希算法
transient int hashSeed = 0;
复制代码

HashMap constructor

We look at four constructor HashMap source for us.

//(1)无参构造器:
//构造一个空的table,其中初始化容量为DEFAULT_INITIAL_CAPACITY=16。加载因子为DEFAULT_LOAD_FACTOR=0.75F
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
复制代码
//(2)指定初始化容量的构造器
//构造一个空的table,其中初始化容量为传入的参数initialCapacity。加载因子为DEFAULT_LOAD_FACTOR=0.75F
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
复制代码
//(3)指定初始化容量和加载因子的构造器
//构造一个空的table,初始化容量为传入参数initialCapacity,加载因子为loadFactor
public HashMap(int initialCapacity, float loadFactor) {
    //对传入初始化参数进行合法性检验,<0就抛出异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //如果initialCapacity大于最大容量,那么容量=MAXIMUM_CAPACITY
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //对传入加载因子参数进行合法检验,
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        //<0或者不是Float类型的数值,抛出异常
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
	//两个参数检验完了,就给本map实例的属性赋值
    this.loadFactor = loadFactor;
    threshold = initialCapacity;
    //init是一个空的方法,模板方法,如果有子类需要扩展可以自行实现
    init();
}
复制代码

From the above three methods, we can find configuration although the initial capacity specified size, but at this time the table is empty, an empty array, and expansion below the predetermined threshold for a given capacity or the default capacity (the first two configuration the third method calls are actually done by). Put before its operation, it will create an array (using the no-argument constructor with the jdk8 similar time).

//(4)参数为一个map映射集合
//构造一个新的map映射,使用默认加载因子,容量为参数map大小除以默认负载因子+1与默认容量的最大值
public HashMap(Map<? extends K, ? extends V> m) {
    //容量:map.size()/0.75+1 和 16两者中更大的一个
    this(Math.max(
        	(int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY), 
         DEFAULT_LOAD_FACTOR);
    inflateTable(threshold);
    //把传入的map里的所有元素放入当前已构造的HashMap中
    putAllForCreate(m);
}
复制代码

The constructor method is invoked inflateTable put before the operation, the specific role of this method is to create a new table for later use elements putAllForCreate loaded in the passed map, we look at this method, but also pay attention to just in this case the expansion mentioned threshold is a threshold of the initial capacity. Next, some of which will be described

(1) inflateTable Method Description

This method is more important, to call this method in the fourth constructor. If you create and use when the object is a collection of the first three, then calls the constructor when calling the put method initialize method table

private void inflateTable(int toSize) {
    //返回不小于number的最小的2的幂数,最大为MAXIMUM_CAPACITY,类比jdk8的实现中的tabSizeFor的作用
    int capacity = roundUpToPowerOf2(toSize);
	//扩容阈值为:(容量*加载因子)和(最大容量+1)中较小的一个
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //创建table数组
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}
复制代码

(2) roundUpToPowerOf Method Description

private static int roundUpToPowerOf2(int number) {
    //number >= 0,不能为负数,
    //(1)number >= 最大容量:就返回最大容量
    //(2)0 =< number <= 1:返回1
    //(3)1 < number < 最大容量:
    return number >= MAXIMUM_CAPACITY
        ? MAXIMUM_CAPACITY
        : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
//该方法和jdk8中的tabSizeFor实现基本差不多
public static int highestOneBit(int i) {
    //因为传入的i>0,所以i的高位还是0,这样使用>>运算符就相当于>>>了,高位0。
    //还是举个例子,假设i=5=0101
    i |= (i >>  1); //(1)i>>1=0010;(2)i= 0101 | 0010 = 0111
    i |= (i >>  2); //(1)i>>2=0011;(2)i= 0111 | 0011 = 0111
    i |= (i >>  4); //(1)i>>4=0000;(2)i= 0111 | 0000 = 0111
    i |= (i >>  8); //(1)i>>8=0000;(2)i= 0111 | 0000 = 0111
    i |= (i >> 16); //(1)i>>16=0000;(2)i= 0111 | 0000 = 0111
    return i - (i >>> 1); //(1)0111>>>1=0011(2)0111-0011=0100=4
    //所以这里返回4。
    //而在上面的roundUpToPowerOf2方法中,最后会将highestOneBit的返回值进行 << 1 操作,即最后的结果为4<<1=8.就是返回大于number的最小2次幂
}
复制代码

(3) putAllForCreate Method Description

This method is traversing element passed map set and then map the present example. Let us look at the implementation details of the method

private void putAllForCreate(Map<? extends K, ? extends V> m) {
    //实际上就是遍历传入的map,将其中的元素添加到本map实例中(putForCreate方法实现)
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putForCreate(e.getKey(), e.getValue());
}
复制代码

putForCreate principle of the method to achieve

private void putForCreate(K key, V value) {
    //判断key是否为null,如果为null那么对应的hash为0,否则调用刚刚上面说到的hash()方法计算hash值
    int hash = null == key ? 0 : hash(key); 
    //根据刚刚计算得到的hash值计算在table数组中的下标
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //hash相同,key也相同,直接用旧的值替换新的值
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            e.value = value;
            return;
        }
    }
	//这里就是:要插入的元素的key与前面的链表中的key都不相同,所以需要新加一个结点加入链表中
    createEntry(hash, key, value, i);
}
复制代码

(4) createEntry implemented method

void createEntry(int hash, K key, V value, int bucketIndex) {
    //这里说的是,前面的链表中不存在相同的key,所以调用这个方法创建一个新的结点,并且结点所在的桶
    //bucket的下标指定好了
    Entry<K,V> e = table[bucketIndex];
    /*Entry(int h, K k, V v, Entry<K,V> n) {value = v;next = n;key = k;hash = h;}*/
    table[bucketIndex] = new Entry<>(hash, key, value, e);//Entry的构造器,创建一个新的结点作为头节点(头插法)
    size++;//将当前hash表中的数量加1
}
复制代码

How to determine the position of the element in the tub

Algorithm and implementation hash values ​​calculated in 1.7 1.8 are not the same, and the hash value in turn related to the position we put new elements, get find elements, remove elements deleted by indexFor time to find the next mark. So we look at these two methods

(1) hash method

final int hash(Object k) {
    int h = hashSeed;
    //默认是0,不是0那么需要key是String类型才使用stringHash32这种hash方法
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    //这段代码是为了对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。简单点
    //说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对
    //最终得到的结果产生影响
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
复制代码

Through the following example to illustrate the importance of disturbance processing for key hashCode, we now want to put a map in a Key-Value Dui, Key value "fsmly", without any disturbance mere knowledge processing after obtaining the simple hashcode, a value obtained " 0000_0000_0011_0110_0100_0100_1001_0010 ", Table if the current array lengths for the map 16, index 10 is the result finally obtained. Since the binary 15 is extended to 32 is "00000000000000000000000000001111", so a number in his bitwise and when in operation before 28 Whatever, the results are the same (as 0 and any number of do and the results are 0, so that put a node on the Entry too dependent on low value of hashCode key, the probability of conflict will greatly increase). As shown below

Because the length of the array map is limited, a large probability of such a collision method is not suitable for use, it is necessary to reduce the processing hashCode perturbing the probability of collision, while the JDK7 for this calculation process using four bits, or by the following a look at a simple example of this process can be seen, not just after the disturbance processing is processing hashCode no hash conflict.

To summarize: we will first calculate the hash value of the incoming key and then determine the position of the table in the following indexFor method, the specific implementation is through a calculated hash value and length-1-bit computing to do, then for 2 ^ n, the length minus one then converted to a binary ONE is low (length 16, len-1 = 15, is binary 1111). The advantage of this set of four above the disturbance is, for every one will get the hashCode affects our index to determine the location, its purpose is to make better data to a different hash buckets, reducing hash conflicts occur. There are more details about the principles and methods of Java hash set, please refer to this hash () method analysis

(2) indexFor Method

static int indexFor(int h, int length) {
    //还是使用hash & (n - 1)计算得到下标
    return h & (length-1);
}
复制代码

hash value and the length of the array is to map primarily to achieve the calculated key for the length-1 bitwise AND, obtained put the Entry array subscripts in the table. A specific calculation procedure when the hash method described above is also an example, not go into here.

put analysis method

(1) put the method

public V put(K key, V value) {
    //我们知道Hash Map有四中构造器,而只有一种(参数为map的)初始化了table数组,其余三个构造器只
    //是赋值了阈值和加载因子,所以使用这三种构造器创建的map对象,在调用put方法的时候table为{},
    //其中没有元素,所以需要对table进行初始化
    if (table == EMPTY_TABLE) {
        //调用inflateTable方法,对table进行初始化,table的长度为:
        //不小于threshold的最小的2的幂数,最大为MAXIMUM_CAPACITY
        inflateTable(threshold);
    }
    //如果key为null,表示插入一个键为null的K-V对,需要调用putForNullKey方法
    if (key == null)
        return putForNullKey(value);
    
    //计算put传入的key的hash值
    int hash = hash(key);
    //根据hash值和table的长度计算所在的下标
    int i = indexFor(hash, table.length);
    //从数组中下标为indexFor(hash, table.length)处开始(1.7中是用链表解决hash冲突的,这里就
    //是遍历链表),实际上就是已经定位到了下标i,这时候就需要处理可能出现hash冲突的问题
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //hash值相同,key相同,替换该位置的oldValue为value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            //空方法,让其子类重写
            e.recordAccess(this);
            return oldValue;
        }
    }
	//如果key不相同,即在链表中没有找到相同的key,那么需要将这个结点加入table[i]这个链表中
    
    //修改modCount值(后续总结文章会说到这个问题)
    modCount++;
    //遍历没有找到该key,就调用该方法添加新的结点
    addEntry(hash, key, value, i);
    return null;
}
复制代码

(2) putForNullKey Analysis Method

This is the case where the processing key is null, the key is null when the incoming time, will begin to traverse the table [0] position, the current actually traversed in table [0] is the head of the linked list of nodes, If the key to find the list of nodes is null, then directly replace the old value of the incoming value. Otherwise, the node creates a new position and added to table [0] position.

//找到table数组中key为null的那个Entry对象,然后将其value进行替换
private V putForNullKey(V value) {
    //从table[0]开始遍历
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        //key为null
        if (e.key == null) {
            //将value替换为传递进来的value
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue; //返回旧值
        }
    }
    modCount++;
    //若不存在,0位置桶上的链表中添加新结点
    addEntry(0, null, value, 0);
    return null;
}
复制代码

(3) addEntry Analysis Method

The method addEntry main role is to determine whether the current size is larger than the threshold value, then determines whether the result of the expansion, eventually creating a new node (the fact that the array table index specified position) is inserted into the head of the list

/*
	hashmap采用头插法插入结点,为什么要头插而不是尾插,因为后插入的数据被使用的频次更高,而单链表无法随机访问只能从头开始遍历查询,所以采用头插.突然又想为什么不采用二维数组的形式利用线性探查法来处理冲突,数组末尾插入也是O(1),可数组其最大缺陷就是在于若不是末尾插入删除效率很低,其次若添加的数据分布均匀那么每个桶上的数组都需要预留内存.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
    //这里有两个条件
    //①size是否大于阈值
    //②当前传入的下标在table中的位置不为null
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //如果超过阈值需要进行扩容
        resize(2 * table.length);
        //下面是扩容之后的操作
        //计算不为null的key的hash值,为null就是0
        hash = (null != key) ? hash(key) : 0;
        //根据hash计算下标
        bucketIndex = indexFor(hash, table.length);
    }
    //执行到这里表示(可能已经扩容也可能没有扩容),创建一个新的Entry结点
    createEntry(hash, key, value, bucketIndex);
}
复制代码

(4) the method of summary execution flow put

  1. First, determine whether the array is empty, as if the air-conditioning expansion with inflateTable.
  2. Then determines whether the key is null, is null if the method is called putForNullKey put. (HashMap described herein also allow the key is null, the default table is inserted in the position at 0)
  3. Call hash () method, once the hash calculation key, hash value and the current array lengths & obtained was calculated array index
  4. Then traverse the list in the array index, the same as if the hash key and passed the hash key and back key of equals true, then the direct coverage value
  5. Finally if not, then this list is inserted in the head to create a new node

resize method analysis

(1) resize process substantially

void resize(int newCapacity) {
    //获取map中的旧table数组暂存起来
    Entry[] oldTable = table;
    //获取原table数组的长度暂存起来
    int oldCapacity = oldTable.length;
    //如果原table的容量已经超过了最大值,旧直接将阈值设置为最大值
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
	//以传入的新的容量长度为新的哈希表的长度,创建新的数组
    Entry[] newTable = new Entry[newCapacity];
    //调用transfer
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    //table指向新的数组
    table = newTable;
    //更新阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
复制代码

(2) transfer analysis method

The method of transfer of all the old array to traverse Entry, head inserted individually recalculated index stored in a new array in accordance with the new capacity.

void transfer(Entry[] newTable, boolean rehash) {
    //新数组的长度
    int newCapacity = newTable.length;
    //遍历旧数组
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                //重新计算hash值
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //这里根据刚刚得到的新hash重新调用indexFor方法计算下标索引
            int i = indexFor(e.hash, newCapacity);
            //假设当前数组中某个位置的链表结构为a->b->c;women 
            //(1)当为原链表中的第一个结点的时候:e.next=null;newTable[i]=e;e=e.next
            //(2)当遍历到原链表中的后续节点的时候:e.next=head;newTable[i]=e(这里将头节点设置为新插入的结点,即头插法);e=e.next
            //(3)这里也是导致扩容后,链表顺序反转的原理(代码就是这样写的,链表反转,当然前提是计算的新下标还是相同的)
            e.next = newTable[i]; 
            newTable[i] = e;
            e = next;
        }
    }
}
复制代码

The main part of this method is that, after the re-calculated hash list structure for the difference of the original and the new table in the list, we understand the following simple FIG, assuming that the position of the original table 4 is a list entry1-> entry2-> entry3, three nodes is calculated at the new scale array or 4, then this process is probably as shown in FIG.

(3) resize expansion Methods

  1. Create a new array (length of 2 times the original length, if it has exceeded the maximum value is set to maximum)
  2. Call transfer method of the entry is moved from the old table into the new array, specific details are set as shown above
  3. The table to the new table, updated threshold value

get analysis method

//get方法,其中调用的是getEntry方法没如果不为null就返回对应entry的value
public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
}
复制代码

We can see, get method is invoked to Entry getEntry query object, and then return the value of Entry. So take a look at the following methods to achieve getEntry

getEntry method

//这是getEntry的实现
final Entry<K,V> getEntry(Object key) {
    //没有元素自然返回null
    if (size == 0) {
        return null;
    }
	//通过传入的key值调用hash方法计算哈希值
    int hash = (key == null) ? 0 : hash(key);
    //计算好索引之后,从对应的链表中遍历查找Entry
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        //hash相同,key相同就返回
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}
复制代码

getForNullKey way

//这个方法是直接查找key为null的
private V getForNullKey() {
    if (size == 0) {
        return null;
    }
    //直接从table中下标为0的位置处的链表(只有一个key为null的)开始查找
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        //key为null,直接返回对应的value
        if (e.key == null)
            return e.value;
    }
    return null;
}
复制代码

jdk1.7 version of the simple summary

(1) because it is put on the operation key is null scene using a single way to do putForNullKey process, HashMap allows null as Key

(2) When calculating the target at the table, according to a call hashcode hash key value () Gets the value of the hash array length-1 for & calculation, the length-1 bits are all 1 After the method, which is able to uniformly distributed, to avoid conflicts (length requirement is an integer of a power of 2) (3) and either get or put a resize, the implementation process will be on the key hashcode hash calculation, and the variable object that is easy to change hashcode, it is recommended to use HashMap immutable objects (such as String type) as the Key. (4) HashMap is not thread-safe in a multithreaded environment when the expansion could result in an infinite loop circular linked list, so if you need multi-threaded operating scenarios can use of ConcurrentHashMap (let's show you simply by way of illustration this case) (5) when a collision occurs, using the HashMap link address conflict processing method (6) set to the HashMap initial capacity 16, it is simply that the expansion of the threshold value 6 8 , the threshold value is too small leads to frequent expansion; and then 32 may space utilization rate.

Problems in the circular linked list illustrated in concurrency jdk7

When it comes to resize the above method, we also explain the process of a resize illustrated by examples, so here we are no longer single-threaded demonstration following the execution flow. We remember the first few lines of the core code resize method

Entry<K,V> next = e.next;
//省略重新计算hash和index的两个过程...
e.next = newTable[i]; 
newTable[i] = e;
e = next;
复制代码

transfer method resize method called main lines of code is the four lines above it, down to simple assumptions about simulation and thread2 execute two threads thread1 resize the process.

Before (1) resize, assuming the length of table 2, it is assumed now add a entry4, requires expansion of the

(2) Suppose now performed to thread1 ** Entry <K, V> next = e.next; ** This line of code, then a few lines of code in accordance with the above, we simply make a note

(3) Since the thread scheduling turn thread2 then performed, assuming thread2 after executing transfer method (assuming entry3 entry4 and after expansion to a position as shown, and here we focus on entry1 entry2 two nodes), then to give the result is

(4) continue to be scheduled at this time thread1, entry1 will go into a new array, then e is Entry2, when the next turn of the next cycle due to the operation becomes a Entry1 Thread2

  • ** First performed newTalbe [i] = e; ** thread1 performed at the time, e is directed entry1
  • Then e = next, resulting in a point to e entry2, (Next entry2, is directed)
  • And the next cycle of next = e.next, (i.e., next = entry2.next = entry1 as a result of execution thread2) pointing to the next results in a entry1

As shown below

(5) thread1 proceed, entry2 will win, place newTable [1] a first position of the bucket, and then move the next e

(. 6) e.next NewTable = [. 1] leads entry1.next entry2, pointing , is also noted at this time has been pointed entry2.next entry1 (thread2 execution result is entry2-> entry1, see executing the above thread2 a schematic view), on the circular linked list, so there.

Guess you like

Origin juejin.im/post/5d416a196fb9a06ada548fc8