常用集合类相关知识点总结

常用集合类相关知识点总结

Android小彩虹2021-08-17 13:36:23220A+A-

前言

复习、复习、复习

昨天被一个大佬问到一些问题,有些问题确实没有思考过,集合类相关的源码大多看过,有些看过不止两遍,都针对画过图,but but ..... 例如常见的HashMap,往深里问思想,如果没看过相关解释,可能就想不出来。程序员应该不止于代码,思维也很重要。

HashMap的是怎么扩容的,它的Hash值是怎么计算的?index位置又是如何计算的?为什么要这么算?为什么扩容是 2 倍 而不是 3 倍、4 倍呢?重写的hashCode 有什么作用?

LinkedHashMap 数据结构是 什么样的? LruCache 内部存储结构又如何?是怎样存的?

接下来会先针对以上问题进行分析解答,然后再分析一些常用的集合类(分析时候只会贴一些关键代码)例如 SparseArrayArrayMap等,以后被问到性能优化也可以谈一谈这些。

环境 : Android API 29

目录

一、HashMap

1、初始变量

loadFactor = 0.75 负载因子

threshold = 16 * 0.75 = 12 在存储完元素后会根据该值判断是否需要扩容

table size = 16

TREEIFY_THRESHOLD = 8 链表转换成红黑树时机

UNTREEIFY_THRESHOLD = 6 红黑树转换成链表时机

MIN_TREEIFY_CAPACITY = 64 树行化时机

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
resize() 方法
当通过new HashMap() 创建时,put元素时会调用resize方法,进行一个元素大小等的设置
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

为什么负载因子是 0.75?

负载因子为0.75时,空间利用率比较高,而且避免了比较多的hash冲突,表现为链表或者是红黑树的高度比较低,设置其它合适的值也可以,但是0.75更遵循泊松分布。数组越大,hash冲突不就越少么,但是数组也不能太大了。

2、存放数据

put(K key, V value)

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

首先看一下 其中的 hash(key)

static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里首先计算keyhashCodeh,然后 与 h 无符号右移 16 位后 异或 得到最后的 hash值。

为什么要使用 hashCode 去操作获取 hash 值?

如果直接用key去操作获取肯定不行,因为key可能会是字符串,字符串不能做位运算,对象类型也不能

为什么要无符号右移 16 位呢?

右移 16 位取出来的 是 h 的高 16 位,也就是说 这里是进行了 h 的高 16 位 和 h 的 低 16 位作异或处理,可以将 h 的 高 位和低位信特征混合保存起来(源码内针对这个方法有解释)。计算元素存放位置是通过 (n - 1) & hash 来计算的, 这里 & 运算相当于 取余运算 hash % n (为什么呢?当n是 2 的幂次方是, n % m = m & ( n - 1) ),假如table大小为16,不做右移运算,那么 可能多个 key.hashCode 都是一样的,例如最后四位都是 0000,都与 1111( n -1 = 15 二进制)& 运算,得到的元素存放位置都是 0 ,如果做了右移异或操作,保留了高位和低位的特征能更好的体现key.hashCode特征,将能降低hash碰撞冲突。

为什么使用异或操作?

如果使用 & ,计算出来的结果会偏向 1 ,采用 | ,计算出来的结果会偏向 0 ,采用 ^ ,偏向 0 和 偏向 1 概率一样。

它和HashTable的区别在于它当keynull 时,hash方法会返回 0 ,而 HashTable 直接通过 key.hashCode() ,所以HashTable key不能为空。

接下来看 putVal 方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
     Node<K,V>[] tab; Node<K,V> p; int n, i;
     if ((tab = table) == null || (n = tab.length) == 0)
          n = (tab = resize()).length;
     if ((p = tab[i = (n - 1) & hash]) == null)
        // 注释 1 如果 对应存储位置没有存Node数据
          tab[i] = newNode(hash, key, value, null);
     else {
          Node<K,V> e; K k;
          if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
              e = p; //注释 2 如果 找到了对应的key则更新数据
           else if (p instanceof TreeNode)//注释 2 如果 是树节点 进行红黑数操作,后面分析
               e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
           else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                       //注释 3  寻找最后一个节点 给next节点创建Node
                        p.next = newNode(hash, key, value, null);
                        //注释 4 如果 bitCount >= 8 - 1 ,转换成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;//交换 下一轮for 操作
                }
            }
          if (e != null) { // existing mapping for key
               V oldValue = e.value;
               if (!onlyIfAbsent || oldValue == null)
                   e.value = value;
               afterNodeAccess(e);
               return oldValue;
          }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
 }

寻找table存储位置是怎么做到的?

通过 ( n - 1) & hash 值得到位置,见注释 1 处

这么做和 hash % n 有什么区别 ,为什么要取模?

n是 2 的幂次方时,两者结果一样,& 操作 比 取余 运算速度快,因为取余操作,底层也要转换成位运算。因为key.hashCode取出来的hash值不是数组的索引下标,为了能随机的计算出索引下标,常见的hash算法就是取模。

如何寻找到已有的key上的元素?

通过计算的hash值=该元素的hash值 并且key相同或者是两者equals,其实可以理解为先判断两者是否hashCode一样,然后判断是否equals。如果不同对象所含的元素一样,认为这是同一个key,就必须得重写hashCodeequals方法。(见上述代码注释 2 )

hash冲突怎么处理?

见注释 3 处

采用链表形式,获取到存储位置的链表,在末尾追加一个新节点,如果发现table扩容到大于64并且链表长度大于 8 了,就会自动转成红黑树,往红黑树里添加一个新节点

为什么要等到table大小大于64并且链表长度大于 8 转成 红黑树?转成其它数据结构或者不转可不可以?

因为红黑树占用空间比链表大,转化也比较耗时,数组容量小的情况可以优先选择充分利用数组,扩容来处理冲突。

针对为什么是8,源码注释有说明,当hashCode离散性很好的时候,树形化用到的概率很小,因为数据均匀分布在每个链表中,几乎不会有链表长度超出阈值,但是在随机 hashCode情况下,离散性差,JDK又不能阻止这种离散性比较差的算法,导致不均匀的分布。不过在理想情况下,随机hashCode 下的链表节点遵循泊松分布,源码中给出了链表长度达到 8 时的概率时 0.00000006,几乎是不可能事件,所以选择了8,完全是根据概率统计决定的。

至于为什么是选择红黑树,查询复杂度,链表是 O(N),而 红黑是是 O(logN),绘制函数曲线,在 N >= 8 时,红黑树表现好。O(logN)的查询速度超过 8 时 比 O(N)情况好,但是使用红黑树内存空间将会是链表的2倍。

根据源码注释,个人理解为这里是先根据概率论选择了阈值8,再确定选择了红黑树结构

其中删除元素时,红黑树转链表的阈值为6,主要是因为,如果也将该阈值设置于8,那么当hash碰撞在8时,会反生链表和红黑树的不停相互激荡转换,白白浪费资源。

3、扩容

java 8 之前,HashMap初始化时会默认容量16,java 8 中,HashMap默认容量为 0,只有第一次put元素时,才会扩容到 16。

if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
    newThr = oldThr << 1;

扩容 resize() 方法最关键之一就是上述代码,扩容2倍。

扩容时会创建一个新数组newTab,重新根据负载因子计算扩容阈值,然后复制数组到新数组中,原来的数据都会置为null,复制过程中分两种情况 : 当前index位置只有一个Node元素,直接复制到新table的新位置;当前位置是链表,如果是红黑是,进行红黑树的移动做法,如果不是,则判断当前链表是否需要移动,不需要则直接存放到新表的index+oldCap位置 (因为扩容是翻倍的,所以与原来的 (n-1) & hash 计算相比,只是多了一个bit 位,所以新位置要么就是原来的位置,要么就是 原位+旧容量位置)。

新位置寻找,会根据该Node元素的hash值,然后重新 与 newTab的大小减 1 作 & 运算(e.hash & (newCap - 1) )

因为之前确定数组下标是通过 hash & (n-1) 操作的,假如 n = 16,就是 hash & 111,然后在扩容时,扩容一倍,大小为16,新的数组下标是 hash & 1111,如果 hash & 1000 (oldCap旧数组大小16) == 0 ,说明高位是0,高位是0,那么说明 hash & 1111 == hash & 111 啊,这不就巧了吗?这种情况数组就在原位置,直接把数据放在对应的index即可。如果 hash & 1000 == 1,说明高位是1hash & 1111hash & 111的区别?大了不?大了多少?1000 == 16 == 之前数组的大小,则数组index就需要往后移 1000(二进制) + 原位置。

 for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }

为什么扩容是2倍而不是3倍,4倍呢?

因为扩容成2倍,数组大小依旧是2的幂次方,2的幂次方-1,它的二进制低位全部都是1,这样保证了hash & (n-1) 后能够完整的保留hash的低位,避免不同的hash值过来后被分配到一样的卡槽中。

二、LinkedHashMap

LinkedHashMap 继承自HashMap ,双向链表结构,重写了HashMapput元素等操作时调用的方法(afterNodeAccess、afterNodeInsertion)、创建Node节点的方法。如果开启了访问顺序accessOrder,则每次被访问的数据都会放到链表最后。

put元素时,先会放到链表Node节点的next指针上,然后更换节点的afterbefore指向,将该元素放到链表最后。

结构图

为什么要新建一个继承HashMapLinkedHashMap呢?

HashMap是无序的,当我们希望有序去存储key - value 时,就可以使用LinkedHashMap。并且相对HashMap增加了一个优点,LinkedHashMap内部维护了一个双向链表,所以在判断哈希表中是否存在某个键值对的时候,可以对链表进行遍历查找,不需要在整个数组桶里面找。

LinkedHashMap 可以设定访问模式,其中 getput都算对元素的访问,会将访问的节点移动到链表末尾。常见的LruCache就是利用了LinkedHashMap,达到最近最少使用策略。

三、LruCache

请设计一个LruCache 缓存机制

public class MyLruCache<K, V> {
    private int cacheSize;
    private float factory = 0.75f;
    private LinkedHashMap<K, V> cacheMap;
​
    public MyLruCache(int cacheSize) {
        this.cacheSize = cacheSize;
        cacheMap = new LinkedHashMap<>(cacheSize, factory, true);
    }
​
    public V get(K key) {
        if (cacheMap.containsKey(key)) {
            return cacheMap.get(key);
        }
        return null;
    }
​
    public void put(K key, V value) {
        cacheMap.put(key, value);
    }
    
}

最近最少使用

LruCache就是一个持有一定数量强引用数据的缓存。当访问一个数据时,这个数据就会被移动到数据队列的头部(经常用到的数据),当数据添加到缓存满时,队列尾部的数据(也就是不常用到的数据)会被删除并被回收。

笔者环境是API 29LruCahceget()方法中取数据代码如下,可以说是访问时,数据会被移动到队列的尾部。

        synchronized (this) {
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }

put方法中,每次都会执行 trimToSize 方法,利用while死循环,来找到链表末尾的数据,并删除。查找代码如下

Map.Entry<K, V> toEvict = null;
                for (Map.Entry<K, V> entry : map.entrySet()) {//LinkedHashMap是有序的
                    toEvict = entry;
                }
                // END LAYOUTLIB CHANGE
​
                if (toEvict == null) {
                    break;
                }
​
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);

Glide中LruCache缓存是如何做的?(后续会专门分析Glide的编解码和缓存)

它有一个ResoureLruCache和一个DiskLruCache

首先,你需要明确一点,Glide中的 LruCache 不是 android util包下的,初始容量是100,而LruCache最大容量是根据屏幕大小和可用内存来计算的,它是在资源释放时缓存。KeyEngineKey类型,重写了equalshashCode方法,ValueEngineResource类型,

其中EngineKey是保存了网络地址url或者是Bitmap和图片的宽高、裁剪参数等信息的。EngineResource是根据服务器返回的数据,按照裁剪参数等包装成的一个类。

每次put数据时,和android util包不一样,

  public synchronized Y put(@NonNull T key, @Nullable Y item) {
    final int itemSize = getSize(item);
    if (itemSize >= maxSize) {  //注释1 所有存入的Bitmap的大小要比你设定的最大缓冲值还大
      onItemEvicted(key, item);   // 这个方法是提供给子类用,当前Bitmap被清除
      return null;
    }
​
    if (item != null) {
      currentSize += itemSize; // 注释2 记录当前大小
    }
    @Nullable final Y old = cache.put(key, item); //注释 3 这里就是LinkedHashMap put了,把元素放到链表队列末尾
    if (old != null) {
      currentSize -= getSize(old);
​
      if (!old.equals(item)) {
        onItemEvicted(key, old); //注释3 清楚old bitmap
      }
    }
    evict(); //注释4 这里调用的是 trimToSize 开始限制容量
​
    return old;
  }

trimToSize() 方法删除是从 LinkedHashMap队列头删除,链表队列头作为最近不常使用的元素。因为你每次getput都是将元素移动到链表末尾。

  protected synchronized void trimToSize(long size) {
    Map.Entry<T, Y> last;
    Iterator<Map.Entry<T, Y>> cacheIterator;
    while (currentSize > size) {
      cacheIterator  = cache.entrySet().iterator();
      last = cacheIterator.next();
      final Y toRemove = last.getValue();
      currentSize -= getSize(toRemove);
      final T key = last.getKey();
      cacheIterator.remove();
      onItemEvicted(key, toRemove);
    }
  }

那它的DiskLruCache是如何实现缓存的?

通过工厂模式和构建者模式,创建了DiskCache的包装类DiskLruCacheWrapper来对外使用,数据经过解码后将Key映射成SafeKey,内部通过LinkedHashMap存放了safeKey-entry,然后拿到Entry类,然后再创建Editor,将数据写到指定文件,文件名key + .temp + 文件长度形式。取文件的时候,通过safeKey拿到对应的Entry,获取文件。

每次外部调用创建DiskLruCache都是通过内部open方法,创建journal文件,journal文件是用来记录缓存的操作记录的,目录是/data/data/packagename/cache

至于DiskLruCache如何创建journal文件和读取、写入缓存的细节,后续再仔细分析。

四、HashTable

初始容量是11,继承自Dictionaryputget方法都加了 synchronized 修饰,所以是线程安全的,扩容是2n+1,每次数据都是放在链表头部,并且key和value都不能存null

为什么初始容量是11,而HashMap是16?

首先HashTable寻址方法是 (key.hashCode() & 0x7FFFFFFF) % tab.length,这里直接用HashCode,不像HashMap为了增强分散效果而做二次hash。其实扩容的时候,是 oldCapacity * 2 + 1。所以应该能联想到,默认11 就是因为质树求余的分散效果好,如果容量是 2 的幂次方,那么 hashCode & 0x7FFFFFFF 高位将会被忽略。

HashTable每次找到数据,如果没有在table相应index中存储,将会把数据直接放到链表头

 HashtableEntry<K,V> e = (HashtableEntry<K,V>) tab[index];
 tab[index] = new HashtableEntry<>(hash, key, value, e);

五、HashSet

实现了Set接口,内部持有HashMap对象,每次value都是固定的Object对象,HashSetadd(key) 方法就是往HashMap中添加 key - object 数据,方法非线程安全的。

六、SparseArray

可以说SparseArray是为了针对HashMap做的优化,keyint类型的,mKeys数组存放keymValues存放值,每次通过二分法在mKeys中找到值为key的元素,返回在mKeys中的下标,然后根据这个下标去更新、存放值。

那他是如何解决HashMap中的散列碰撞问题的呢?

它通过下标映射方法,规避了这种问题。

需要注意以下几点

**1、**如果调用remove删除元素后,元素会标记DELETE状态,在下一次put元素时,如果容量超过了mKeys.length,会执行gc方法,将mKeysmValues中那些标记了DELETE状态的元素元素删除,数据整体前移。

    private void gc() {
        int n = mSize;
        int o = 0;
        int[] keys = mKeys;
        Object[] values = mValues;
​
        for (int i = 0; i < n; i++) {
            Object val = values[i];
​
            if (val != DELETED) {
                if (i != o) {
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;
                }
​
                o++;
            }
        }
        mGarbage = false;
        mSize = o;
    }

**2、**如果put元素时,没有找到对应的下标,将会对计算出的索引值index取反,返回值要么是0,要么为负数,但是插入数据前也会进行取反操作。通过二分法这种方式,mKeys内部的值一直是按照递增的方式来排列的,因为下面代码的 lo 它最后结果是 lo = hi + 1,所以插入元素到 lo 位置时,后面元素需要整体后移。

static int binarySearch(int[] array, int size, int value) {
        int lo = 0;
        int hi = size - 1;
​
        while (lo <= hi) {
            final int mid = (lo + hi) >>> 1;
            final int midVal = array[mid];
​
            if (midVal < value) {
                lo = mid + 1;
            } else if (midVal > value) {
                hi = mid - 1;
            } else {
                return mid;  // value found
            }
        }
        return ~lo;  // value not present
    }

**3、**没有找到下标,是如何存放元素,即扩容是怎么扩的

public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
        if (currentSize + 1 <= array.length) {
            System.arraycopy(array, index, array, index + 1, currentSize - index);
            array[index] = element;
            return array;
        }
​
        T[] newArray = (T[]) Array.newInstance(array.getClass().getComponentType(),
                growSize(currentSize));
        System.arraycopy(array, 0, newArray, 0, index);
        newArray[index] = element;
        System.arraycopy(array, index, newArray, index + 1, array.length - index);
        return newArray;
    }

通过系统API,将 0 ~ index-1 间的元素复制到 newArray,然后再将下标 index 后的元素复制到 newArrayindex+ 1位置,即将数据插入到index位置,原有数据向后移。

七、ArrayMap

keynull时,hash = 0,也就是说key = nullmArray[0]存放对应的值。通过new ArrayMap() 方式使用,hash值就是 key.hashCode() ,然后得到该hashmHash中的位置。这个有点类似SparseArray了。

那么扩容是怎么实现的呢?

put元素时,如果大小已经超过了mHashes.length,将会判断扩容大小

final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
                    : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE); //BASE_SIZE = 4

超过8个将会扩大自己的 1/ 2。在开始扩容操作之前会检查数据有被操作,例如删除、添加,有的话就会抛 ConcurrentModificationException 异常。然后在 size = 4、 8 时会从缓存里面取 array[0]array[1]赋值给mArraymHashes (这里为什么要用缓存呢?)。然后复制数组到新数组里面,然后将旧的数据放到缓存里面(为了减少对象创建的开销),最后再设置新的数据。

缓存是怎么做的呢?

设置缓存

private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
        if (hashes.length == (BASE_SIZE*2)) {
            synchronized (ArrayMap.class) {
                if (mTwiceBaseCacheSize < CACHE_SIZE) {
                 // 将传过来的 array[0]和array[1]分别保存 mTwiceBaseCache和 hashes
                    array[0] = mTwiceBaseCache;
                    array[1] = hashes;
                    //array所有数据 除了 下标 0 1 的数据都置空
                    for (int i=(size<<1)-1; i>=2; i--) {
                        array[i] = null;
                    }
                    //mTwiceBaseCache 指向 array
                    mTwiceBaseCache = array;
                    mTwiceBaseCacheSize++;
                    if (DEBUG) Log.d(TAG, "Storing 2x cache " + array
                            + " now have " + mTwiceBaseCacheSize + " entries");
                }
            }
        } else if (hashes.length == BASE_SIZE) {
           // mBaseCache 
    }

mTwiceBaseCache结构如下图

取缓存

   private void allocArrays(final int size) {
        if (mHashes == EMPTY_IMMUTABLE_INTS) {
            throw new UnsupportedOperationException("ArrayMap is immutable");
        }
        if (size == (BASE_SIZE*2)) {
            synchronized (ArrayMap.class) {
                if (mTwiceBaseCache != null) {
                //这里其实就是设置缓存的逆过程
                    final Object[] array = mTwiceBaseCache;
                    mArray = array;
                    mTwiceBaseCache = (Object[])array[0];
                    mHashes = (int[])array[1];
                    array[0] = array[1] = null;
                    mTwiceBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
                            + " now have " + mTwiceBaseCacheSize + " entries");
                    return;
                }
            }
        } else if (size == BASE_SIZE) {
            //mBaseCache
        }
​
        mHashes = new int[size];
        mArray = new Object[size<<1];
    }

八、ArrayList

在第一次add数据时,会设置容量为10,后续将扩容1.5倍,删除数据 O(N) 、添加数据O(1)

九、ArraySet

实现了 Collection接口,与ArrayMap一样,区别在于mArray数组,它是直接存值。并不需要存放key

十、LinkedList

链表结构,每次操作都相当于是对链表的操作,查找O(N)、删除O(1)、添加O(1)

十一、ThreadLocal

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

得到当前Thread内部持有的ThreadLocal.ThreadLocalMap对象,调用ThreadLocalMapset方法存值

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

从上述代码可以看出 key 是当前ThreadLocalkeyvalue组成一个Entry存放在table中,从中可以看出,一个线程对应于一个ThreadLocalMap,内部有table数组,存放不同ThreadLocal和对应的Value值,当然一个ThreadLocal可以在不同的线程中使用,即调用set发方法。

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们