本文无废话, 全干货, 由 hashCode 方法开始讲起, 带你完全重新认识 hashCode 方法, 并囊括 HashMap 所有可能的八股文知识, 附带源码详细解读; 然后将会讲解阿里面试问题, 进行知识提炼与提升, 希望大家都能认真看完;

hashCode

Object 的 hashCode 方法, JDK1.8 的默认实现是通过线程状态和移位异或的算法计算出来的, 并不是内存地址; 内存地址是老掉牙的版本的默认实现, 这个问题不要再踩坑了, 说内存地址必挂, 必挂, 必挂 !!!

Java 的哈希算法要求返回一个 32 位长的哈希码, 即一个 int;

Integer 的哈希算法, 返回的就是 Integer 底层的 value值;

Byte 和 Character 的 hashCode , 就是把底层的 value 强转成 int 返回;

Long 的 hashCode, 因为 long 的长度已经超过了32 , 所以截取出 32 位, 并通过移位异或的方式把高位数据带入到低位中, 增加扰动;

return (int)(value ^ (value >>> 32));
  • 1.

Float 和 Double 的 hashCode, 转成对应的 IEEE754 标准的二进制表示, 用 int 或 long 保存; 然后依据 Integer 和 Long 的规则再 hash; 例如 Double, 取二进制表示后 return (int)(value ^ (value >>> 32));

String 的 hashCode, 从最左边的字符到最右边的字符给不同的权值, 每一位的权值是 31 的若干次方, 最高位是 0 次方; 将每一位的权值乘以每一位字符的整型值, 相加得到最终结果 (空字符串的哈希值是0);

如果要用数组的 hashCode, 应该使用 Arrays.hashCode(), 而不是直接调用 数组对象的 hashCode 方法;

Arrays.hashCode(), 其原理和 String 的 hashCode 基本一致, 以 31^k 作为权值, 对每一个元素, 先调用该 hashCode方法, 再乘以每个元素各自的权值; 下标为 n - 1 的元素权值为1, 下标越小权值越大;

下标0的元素有特殊处理, = 权值 * ( 31 + 0号元素的hashCode )

HashMap

  • LinkedHashMap是HashMap的子类, 在HashMap的基础上, 维护了一个双向链表, 该双向链表在记录插入顺序和记录访问顺序之间二选一, 总之就是用来记录顺序, 实现按顺序的迭代; LinkedHashMap 返回的迭代器, 是在双向链表上移动的;
  • HashMap底层实现是Node数组 + 链表 + 红黑树TreeNode, Node是内部类, 实现了Map.Entry, 表示一个键值对;

Hash值

  • 每个Node有一个成员 int hash 来记录这个键值对的哈希值, 这个值由 key.hashCode()经过移位异或计算得出; 如果key是null, hash = 0;
// Object的hashCode方法对equals返回true的对象的哈希值也是一样的
// HashMap定义的hash方法
static final int hash(Object key) {
    int h;
    // Object的hashCode 异或 其无符号右移16位, 为了减少特征损失
    // 例如, 散列时是根据hash值%数组长度, 这样的话相当于只有低若干位真正起到了区分效果
    // 所以异或右移后的结果, 将高位信息带入一些到低位中
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 多加一步移位异或, 是为了增加扰动;
  • 计算下标时, 本应通过取模来做, 但考虑到效率较低, 用取与代替, 并且将数组长度设置为2的整数次幂, 来保证取模和 n - 1 取与的结果一致; 另外长度为 2 的整数次幂, 在扩容时, 50% 的结点不需要移动位置, 后面讲低位树会讲到;

HashMap的成员

// 底层数组, 初始为null
transient Node<K,V>[] table;
// 记录修改数, 防止并发修改
transient int modCount; 
// 用于扩容
int threshold; 
// 装填因子, 默认值0.75
final float loadFactor; 

// 红黑树相关
static final int TREEIFY_THRESHOLD = 8; // 大于8且容量大于等于64时树化
static final int UNTREEIFY_THRESHOLD = 6; // 小于等于6链表化
static final int MIN_TREEIFY_CAPACITY = 64;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

遍历

  • 三种遍历方式: entrySet(), values(), keySet(), 和 ArrayList 类似, 这三个方法所返回的集合上的 Iterator 的移动都是直接在原本的HashMap对象上进行的;

这里可能有同学会看不懂, 关于 ArrayList 和 Iterator, 后面会再出专门的文章讲解;

  • 这仨方法的返回值, 并不重要, 重要的是在返回值上取到的 Iterator, 为什么三种方式的遍历结果有的能拿到value, 有的能拿到整个 Entry ? 归根结底在于 Iterator 的 next 方法的返回值不同; 这仨的 iterator 都继承自HashIterator:
abstract class HashIterator {
    Node<K,V> next;        // next entry to return, 相当于cursor
    Node<K,V> current;     // current entry, 相当于lastRet
    int expectedModCount;  // for fast-fail, 并发修改异常, 和ArrayList一样
    int index;             // current slot, 记录当前Node数组下标的位置
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • entrySet() 返回一个Set\, 实际上也就是\, set.iterator() 返回一个迭代器, 该迭代器利用Node的next指针, 遍历一个下标位置的链表, 指向null了就 table[slot++] 遍历下一个链表, 从而完整遍历Node数组; 其next方法返回Node结点的引用;
  • values原理一样的, 只不过它的iterator.next方法返回的是node.value
  • KeySet也是一样, 它的iterator.next方法返回的是node.key
  • 这三个视图的iterator都有expectedModCount, 作用原理和ArrayList一样
扩容
  • 初始容量:
    除直接从集合构造外, 其余构造函数不会分配实际内存空间, 都是在首次添加元素的时候分配空间;
    无参构造, threshold = 0; 分配空间时容量设为16;
    有参构造指定了 initialCap 时, 最终会调用 tableSizeFor方法, 取一个 >= initialCap 的 2 ^ n 作为容量, 最大为 1<<30;
    从集合 c 构造, 由 c.size() 除以自己的 loadFactor 装填因子, 再向上取整, 算一下容量最小min是多少, 然后调用tableSizeFor 返回一个 >=min 的 2^n, 然后立即分配空间(并调用putVal方法将源集合 c 中的元素复制过来)
  • 扩容机制: 首次扩容时, 按存到 threshold 里的容量扩容, 并更新 threshold 为阈值;
    其它时候, 当元素个数大于 threshold 时扩容, 直接double原来的阈值threshold; 逻辑上, capacity也一并double
  • HashMap底层数组由len扩容到2*len的时候, 原本在旧数组x位置的元素: 要么重新散列到x位置, 要么在len+x的位置; 原理后面会提到;

HashMap源码解读

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// HashMap支持的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认的装填因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 拉链超过8后转化为红黑树
static final int TREEIFY_THRESHOLD = 8;

// 小于等于6后退化为链表
static final int UNTREEIFY_THRESHOLD = 6;

// 表长度<64时不会树化,而是直接扩容原表
static final int MIN_TREEIFY_CAPACITY = 64;

//--------------------------------成员-------------------------------------

// 底层数组
transient Node<K,V>[] table;

// 实际上是EntrySet对象
transient Set<Map.Entry<K,V>> entrySet;

// 元素个数
transient int size;

// 用于value() 和entrySet() 上的interator
transient int modCount;

// = 当前容量(刚构造完时可能出现还没分配的情况) * loadFactor
int threshold;

// 装填因子
final float loadFactor;

// 无参构造,参数都用默认值, 注意此时初始容量threashold = 0;
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

// 并没有真正分配空间,只是确定参数
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    // 重点,刚构造还没分配空间时, threshold先用作capacity
    // 以后threshold = capacity * 装填因子loadFactor, 当元素个数超过threshold的时候扩容
    this.threshold = tableSizeFor(initialCapacity);
}

// 初始容量确定
// 和ArrayDeque类似,不过先减了1,这样最终结果是 >= cap 的2的整数次幂(power of two)
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    // 假设 n 的最高位是第3位, 即 1xx;
    // 那么右移以后变 1x
    // 取或以后得 11x;
    // 也就是说, 经过这一步, 原本最高位的1, 它的低一位无论以前是多少, 一定会变成1;
    n |= n >>> 1;
    // 同理, 最高位以及后面的 3 位一定会为全1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    // 最高位以及后面的 31 位一定会为全1; 也就是说, 无论 n 是多少, 最终都会变成若干位全1;
    // 最后返回的时候再 + 1, 就变成了 10000000 这种形式, 即 >= cap 的 2^n
    n |= n >>> 16;
    // 注意这时还没+1
    // 如果给的n是0或者负数,左全改成1后小于零,返回1
    // 如果给的n >= 1<<30, 那么现在仍 >= 1<<30, 此时返回 1<<30
    // 否则返回n+1, 也就是 >= 给定值的2的整数次幂
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

// 添加方法最终都会到这来
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // n = table.length
    // 1.首次添加时先resize
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. i = %数组长度, 也就是要放到的位置, 如果这个位置为null, 直接放到table[i]
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        
        Node<K,V> e; K k;
        // 3.如果table[i]已经有元素了, 这时p指向它
        // 3.1如果 p和要加入的Node的一样,认为完全重复,不会加入新结点,直接更新p的value
        //    并将原本的value作为返回值返回
        //    可以看到只有两个Node的hash值一样并且 (key== 或者equals)时, 才认为两个Node一样
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 3.2如果这里已经是红黑树了,将新节点尝试加入红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 3.3如果这里不是红黑树, 尝试加入并检查是否需要树化
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    // 当某一轮p指向第8个节点, 此时binCount=7, 如果这时p的下一个结点为null, 
                    // 那么将新节点放到p.next, 此时共有9个结点
                    p.next = newNode(hash, key, value, null);
                    // binCount=7, 树化, 也就是说拉链长度为9时树化
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果在拉链中发现了完全一致的结点, 结束循环, 用新value更换旧value
                if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // p = e用于遍历链表
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 添加完成后size++并检查元素个数是否超过了阈值, 是则扩容
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

// 扩容机制
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    // 首次分配空间时, oldCapacity = 0; 
    // 2.其余时间 oldCap = 原表长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 1.有参构造首次分配时oldThr = 应该分配的空间
    // 2.无参构造首次添加时, oldThr = threshold = 0
    // 2.其余时间 oldThr就是原本的阈值
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 2.首次扩容以外, 如果已经到最大容量了, 不会再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 2.如果没有达到最大容量, 并且capacity*2后小于最大容量, 并且原容量>=16时, double容量阈值
        // 3.如果原容量 < 16, 例如有参构造先分配了8, 进行判断时执行newCap = oldCap << 1
        // 3.newCap = 16 , newThr = 0; 相当于这里只double了容量
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 1.有参构造首次分配空间时直接走到这
    else if (oldThr > 0)
        // 1.新容量 = 构造时计算出的容量, 应该分配的空间
        newCap = oldThr;
    else { // 4.无参构造时oldThr = 0, 走到这, 按容量16初始化, 跳过下一个if, 直接开始分配
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    // 有参构造首次分配时newTHr == 0,会进入这个if
    if (newThr == 0) {
        // 3.这里double阈值, 好像没什么区别
        float ft = (float)newCap * loadFactor;
        // 1.现在已经分配空间了, threshold不再用作容量, 而是 = cap * 装填因子
        // 2.如果新容量 >= 1<<30了, 那么threshold = int最大值, 也就是说不会再扩容了
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    // newCap的值作为新的总容量, 按其分配空间
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 复制原本的table
    if (oldTab != null) {
        .......
    }
    return newTab;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.

面试: 红黑树转化的阈值是多少?

  • 单个量表长度 大于等于 9 时转化为红黑树 (仅当桶数 大于等于 64时, 因为当数组长度过小的时候, 在添加数据的过程中, 数组会反复扩容, 导致红黑树拆开又变为链表, 反复在红黑树和链表间转化效率不高/意义不大)
  • 记住是 > 8, 也就是 >= 9;
  • 为什么是8?
    这是在时间和空间上的一个权衡 (因为虽然红黑树在结点多的时候查找更快, 但红黑树结点的大小约为链表结点的两倍); HashCode 算法设计理想时, 不同长度链表的出现概率满足泊松分布, 当 Load Factor 为 0.75 时, 链表长度为 9 的可能小于千万分之一, 几乎是不可能发生的;
    因此, 在 hash 算法设计良好的哈希表中, 很少会有红黑树;
  • 扩容时, 中序遍历原本的红黑树中的结点, 将他们重新散列到两个位置, 拆分后新构建的红黑树节点总数小于等于6时转化为链表, 两个阈值不一样是为了避免频繁的转化;
  • 以容量从 4 -> 8 为例, 原本在下标2的一棵树, 里面的hash肯定都是xxxx xx10的形式, 扩容后, 新下标由低三位决定, 原本下标为2的 entry 的 Hash 值有两种情况, xxxx x110 和xxxx x0110, 第三位的值有两种情况, 0或1, 如果是0, 说明在新数组的下标没变(低位树), 是1说明新下标为4+2(高位树)
  • 不仅拆分高低树时会发生反树化, 删除结点时, 如果红黑树根节点为null, 或root.left为null, 或root.right为null, 或root.left.left为null, 都会转回链表
// 添加方法最终都会到这来
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1.首次添加时先resize
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. i = 模数组长度, 也就是要放到的位置, 如果这个位置为null, 直接放到table[i]
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        
        Node<K,V> e; K k;
        // 3.如果table[i]已经有元素了, 这时p指向它
        // 3.1如果 p和要加入的Node的key和hash值都一样,认为完全重复,不会加入新结点,直接更新p的value
        //    并将原本的value作为返回值返回
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 3.2如果这里已经是红黑树了,将新节点尝试加入红黑树
        else if (p instanceof TreeNode)
            // 如果和根节点完全重复, 不做操作, 把企图加入的节点直接返回
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 3.3如果这里不是红黑树, 尝试加入并检查是否需要树化
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    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;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 添加完成后size++并检查元素个数是否超过了阈值, 是则扩容
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.

面试: 什么时候认为两个键值对重复?

  • 两个键值对的 key 满足 (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
  • 需要注意Node有四个成员, hash, key, value, next; 当往哈希表中存放key-value时, 存放时刻key的hash值被保存在Node的hash成员中, 以后再比较时并不会去计算Node中key的hash值, 而是直接用Node.hash
    假设我们的key是一个Student类对象student, 并且重写了hashCode和equals方法, 那么把student作为key放到表中以后再修改student的某一字段, 就可能导致student的hashCode变化, 这时hash(student) != Node.hash, 就没办法找到我们以前插入的key-value了
  • 归根结底, 哈希表是用来查找的, 不要存放可能改变的key

面试: 为什么JDK1.8之后往链表插入新键值对的时候改为尾插法

[HashMap 源码详解] 阿里面经: 不要再说 hashCode 默认实现是取内存地址了, 必挂!_hashCode

面试: 都知道 HashMap线程不安全, 你能举些例子吗?

覆盖
  • 或者说是元素丢失问题
  • 两个线程同时调用put方法, 计算得到了相同的index, 两个线程又同时通过了这个位置为null的判断, 就会发生覆盖问题; 本来应该拉链法解决冲突的, 现在位置上只有一个元素;
异常
  • 当链表长度 > 8时, 将链表转为红黑树, 结点类型由Node转为TreeNode
  • 线程一插入的时候按Node普通链表去遍历查找插入位置
  • 线程二插入完成导致树化
  • 切回线程一, 本来按Node处理, 但是现在已经变成了TreeNode的结构, 抛出异常
扩容时问题
  • resize方法扩容后复制元素时
Node<K,V>[] oldTab = table;
// ....
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
    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;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 假设两个线程同时调用put方法, 最终都进入了resize方法, 线程1先让table = newTab1;
  • 此时切换到线程2, 执行resize方法一开始的 oldTab = table, 现在线程2的 oldTab 直接没有元素了, 复制个寂寞

本文全原创, 创作不易, 如果感觉对您有帮助请点个赞鼓励一下作者;