HashMap1.7深度解析

1.hashmap1.7结构图

总结:

简单来说,HashMap1.7由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

 2.Entry的成员属性

   static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        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;
        }

        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;
        }

        public final int hashCode() {
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

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

        /**
         * This method is invoked whenever the value in an entry is
         * overwritten by an invocation of put(k,v) for a key k that's already
         * in the HashMap.
         */
        void recordAccess(HashMap<K,V> m) {
        }

        /**
         * This method is invoked whenever the entry is
         * removed from the table.
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }

3.我们先看作者对hashmap1.7的解释

/**
 * 基于哈希表的<tt>Map</tt> 接口实现。这个
 * 实现提供了所有可选的地图操作,并允许
 * <tt>null</tt> 值和 <tt>null</tt> 键。 (<tt>HashMap</tt>
 * class 大致相当于<tt>Hashtable</tt>,除了它是
 * 不同步并允许空值。)此类不保证
 * 地图的顺序;特别是,它不保证订单
 * 将随着时间的推移保持不变。
 *
 * <p>这个实现为基本的提供了恒定时间的性能
 * 操作(<tt>get</tt> 和 <tt>put</tt>),假设散列函数
 * 在桶中正确分散元素。迭代
 * 集合视图需要与“容量”成正比的时间
 * <tt>HashMap</tt> 实例(桶的数量)加上它的大小(数量
 * 键值映射)。因此,不要设置初始值是非常重要的
 * 如果迭代性能太低,容量太高(或负载因子太低)
 * 重要的。
 *
 * <p><tt>HashMap</tt> 的一个实例有两个影响它的参数
 * 性能:<i>初始容量</i>和<i>负载系数</i>。这
 * <i>capacity</i> 是哈希表中的桶数,初始
 * 容量只是创建哈希表时的容量。这
 * <i>load factor</i> 是衡量哈希表允许达到多满的指标
 * 在其容量自动增加之前获取。当数
 * 哈希表中的条目超过负载因子和
 * 当前容量,哈希表<i>重新哈希</i>(即内部数据
 * 结构被重建),因此哈希表大约有两倍的
 * 桶数。
 *
 * <p>作为一般规则,默认负载因子 (.75) 提供了一个很好的权衡
 * 时间和空间成本之间。较高的值会减少空间开销
 * 但增加了查找成本(体现在大部分操作
 * <tt>HashMap</tt> 类,包括<tt>get</tt> 和<tt>put</tt>)。这
 * 应采用地图中的预期条目数及其负载因子
 * 在设置其初始容量时考虑在内,以尽量减少
 * 重新哈希操作的次数。如果初始容量更大
 * 比最大条目数除以负载因子,没有
 * 重新哈希操作将永远发生。
 *
 * <p>如果许多映射要存储在一个 <tt>HashMap</tt> 实例中,
 * 以足够大的容量创建它将使映射能够
 * 存储比让它执行自动重新散列更有效
 * 需要扩大表。
 *
 * <p><strong>请注意,此实现不是同步的。</strong>
 * 如果多个线程同时访问一个哈希映射,并且至少有一个
 * 线程在结构上修改地图,它<i>必须</i>
 * 外部同步。 (结构修改是任何操作
 * 添加或删除一个或多个映射;仅仅改变价值
 * 与实例已经包含的键关联的不是
 * 结构修改。)这通常由
 * 同步一些自然封装地图的对象。
 *
 * 如果不存在这样的对象,则应使用
 * {@link Collections#synchronizedMap Collections.synchronizedMap}
 * 方法。这最好在创建时完成,以防止意外
 * 对地图的非同步访问:<pre>
 * Map m = Collections.synchronizedMap(new HashMap(...));</pre>
 *
 * <p>该类的所有“集合视图方法”返回的迭代器
 * 是 <i>fail-fast</i>:如果地图在之后的任何时间进行了结构修改
 * 迭代器以任何方式创建,除了通过迭代器自己的
 * <tt>remove</tt> 方法,迭代器会抛出一个
 * {@link ConcurrentModificationException}。因此,面对并发
 * 修改,迭代器快速干净地失败,而不是冒险
 * 在不确定的时间任意的、非确定性的行为
 * 未来。
 *
 * <p>注意不能保证迭代器的快速失败行为
 * 因为一般来说,不可能做出任何硬性保证
 * 存在不同步的并发修改。快速失败迭代器
 * 尽最大努力抛出 <tt>ConcurrentModificationException</tt>。
 * 因此,编写依赖于此的程序是错误的
 * 其正确性的例外:<i>迭代器的快速失败行为
 * 应仅用于检测错误。</i>
 *
 * <p>这个类是
 * <a href="{@docRoot}/../technotes/guides/collections/index.html">
 * Java 集合框架</a>。
 *
 * @param <K> 此映射维护的键类型
 * @param <V> 映射值的类型
 *
 * @author 道格利亚
 * @author 乔什·布洛赫
 * @作者亚瑟范霍夫
 * @作者尼尔·加夫特
 * @see Object#hashCode()
 * @see 集合
 * @见地图
 * @see TreeMap
 * @see 哈希表
 * @自 1.2
 */

4.HashMap中定义的常量

 /**
 * 默认初始容量 - 必须是 2 的幂。
 */
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
 
/**
 * 最大容量,如果隐式指定了更高的值,则使用
 * 通过任何一个带参数的构造函数。
 * 必须是 2 的幂 <= 1<<30。
 */
 static final int MAXIMUM_CAPACITY = 1 << 30;
 
 /**
 * 在构造函数中未指定时使用的负载因子。
 */
 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 
 /**
 * 表未创建时共享的空表实例。
 */
 static final Entry<?,?>[] EMPTY_TABLE = {};
 
 /**
 * 表格,根据需要调整大小。长度必须始终是 2 的幂。
 */
 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
 
 /**
 * 此映射中包含的键值映射数。也就是有多少key-value对
 */
 transient int size;
 
 /**
 * 要调整大小的下一个大小值(容量 * 负载因子)。
 * 如果 table == EMPTY_TABLE 那么这是初始容量
 * 表将在扩容resize()时创建。
 */
 int threshold;
 
 /**
 * 哈希表的负载因子。
 */
 final float loadFactor;
 
 /**
  * 这个HashMap结构被修改的次数
  * 结构修改是指那些改变映射数量的修改
  * HashMap 或以其他方式修改其内部结构(例如,
  * 重新哈希)。 此字段用于在集合视图上制作迭代器
  * HashMap 快速失败。 (请参阅 ConcurrentModificationException)。
  */
 transient int modCount;
 
 /**
  * 地图容量的默认阈值,高于该阈值的替代散列是
  * 用于字符串键。 替代哈希降低了发生率
  * 由于字符串键的哈希码计算较弱而导致的冲突。
  * <p/>
  * 这个值可以通过定义系统属性来覆盖
  * {@code jdk.map.althashing.threshold}。 {@code 1} 的属性值
  * 强制始终使用替代散列,而
  * {@code -1} 值确保永远不会使用替代散列。
  */    
 static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
 
 /**
  * 与此实例关联的随机化值应用于
  * 键的散列码使散列冲突更难找到。 如果 0 那么
  * 替代散列被禁用。
  */
 transient int hashSeed = 0;

5.构造方法

 public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
 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);
        //加载因子赋值0.75
        this.loadFactor = loadFactor;
		//扩容临界值为16
        threshold = initialCapacity;
        init();
    }
   /**
     * 子类的初始化钩子。这个方法被调用
     * 在所有构造函数和伪构造函数(clone, readObject)
     * 在HashMap被初始化之后但在任何条目被插入之前
     * 被插入。 (在没有这个方法的情况下,readObject会
     * 需要对子类有明确的了解)。
     */
void init() {
}

6.put()方法解析

 public V put(K key, V value) {
		//表格为空
        if (table == EMPTY_TABLE) {
			//初始化数组table
            inflateTable(threshold);
        }
		//key为null放在table[0]的位置
        if (key == null)
            return putForNullKey(value);
		//计算当前key的hash值
        int hash = hash(key);
		//计算存储桶的位置
        int i = indexFor(hash, table.length);
		//判断table[i]是否已经有元素,如果有,就判断是否需要更新
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
			//判断hash值和key是否相同
			//== 比较的是内存地址;equals比较的是内存中的值是否相同
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
				//如果key相同,就做更新操作
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
				返回旧的值
                return oldValue;
            }
        }
		//修改次数+1
        modCount++;
		//添加Entry
        addEntry(hash, key, value, i);
		//如果不是更新,而是插入返回null
        return null;
    }
/**
      * 检索对象哈希码并将补充哈希函数应用于
      * 结果散列,它可以防止质量差的散列函数。 这是
      * 关键,因为 HashMap 使用长度为 2 的哈希表,即
      * 否则会遇到相同的 hashCode 冲突
      * 在低位。 注意:空键总是映射到哈希 0,因此索引 0。
      */
	final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
/**
	* 初始化数组table
	*/
	private void inflateTable(int toSize) {
        // 如果表格为空时,默认的哈希表容量时threshold临界值的两倍
        int capacity = roundUpToPowerOf2(toSize);
        //临界值为
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }
//roundUpToPowerOf2(toSize);
     private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

     /**
	 * hashmap是允许null键null值的,但是null值null键放在table[0]位置上,不会改变
	 */
     private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
			//更新操作
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

	/**
      * 添加具有指定键、值和哈希码的新条目到
      * 指定的桶。 这是这个的责任
      * 调整表格大小的方法(如果合适)。
      *
      * 子类覆盖它以改变 put 方法的行为。
      */
	void addEntry(int hash, K key, V value, int bucketIndex) {
		//如果达到扩容的临界值就扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
			//重新定义数组,是原有数组的两倍
            resize(2 * table.length);
			//重新计算hash值
            hash = (null != key) ? hash(key) : 0;
			//重新计算新table中桶的位置
            bucketIndex = indexFor(hash, table.length);
        }
		//如果不需要扩容,创建Entry对象,放在原有的table中
        createEntry(hash, key, value, bucketIndex);
    }
/**
      * 将此映射的内容重新散列到一个新数组中
      * 更大的容量。 这个方法在调用的时候自动调用
      * 此映射中的键数达到其阈值。
      *
      * 如果当前容量为 MAXIMUM_CAPACITY,则此方法不
      * 调整地图大小,但将阈值设置为 Integer.MAX_VALUE。
      * 这具有防止将来调用的效果。
      *
      * @param newCapacity 新容量,必须是二的幂;
      * 必须大于当前容量,除非当前
      * 容量为 MAXIMUM_CAPACITY(在这种情况下值
      * 无关紧要)。
      */
	void resize(int newCapacity) {
		//因为要扩容,所以把原有的table赋值给oldTable
        Entry[] oldTable = table;
		//原有table的容量
        int oldCapacity = oldTable.length;
		//如果容量已经达到最大值值了,不调整改tbale的大小直接返回
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
		//创建新的数组容量是newCapacity=2*oldCapacity
        Entry[] newTable = new Entry[newCapacity];
		//新的table有了,接下来就是转移旧table中的数据到新的table中oldTable->newTable
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
 /**
      * 返回哈希码 h 的索引。
      */
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

	/**
      * 与 addEntry 类似,只是在创建条目时使用此版本
      * 作为地图构建或“伪构建”(克隆、
      * 反序列化)。 此版本无需担心调整表格大小。
      *
      * 子类覆盖它以改变 HashMap(Map) 的行为,
      * 克隆和读取对象。
      */
	void createEntry(int hash, K key, V value, int bucketIndex) {
		//把e指向table[bucketIndex]桶的位置
        Entry<K,V> e = table[bucketIndex];
		//这里就是jdk1.7头插法的体现,他把上一步的e放在新Entry的next属性上
        table[bucketIndex] = new Entry<>(hash, key, value, e);
		//当前table存储的size+1
        size++;
    }

	 /**
      * 将所有条目从当前表转移到新表。
      */
    void transfer(Entry[] newTable, boolean rehash) {
		//新table的容量
        int newCapacity = newTable.length;
		//遍历旧的table,取出每一个元素
        for (Entry<K,V> e : table) {
			//这里用死循环,因为hashmap是数组+链表,所以这一步就是取出当前拉链上的每一个值
            while(null != e) {
				//!!!!!这里可能导致线程不安全也就是hashmap1.7循环链表的问题,后续讲解
				//next下一个节点
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
				//重新计算桶的位置
                int i = indexFor(e.hash, newCapacity);
				//把e.next指向newTable[i]的位置
                e.next = newTable[i];
				//把e放在i位置上,这里其实就是向下移动的一个操作,后面画图更好理解
                newTable[i] = e;
				//把next赋值给e,继续循环,直到  while(null != e),也就是当前桶的链表的最后一个位置被取出为止
                e = next;
            }
        }
    }

总结:这里陈述了整个put()方法的过程,并且如何初始化table,达到临界值如何扩容,以及扩容后旧数组中的值迁移到新的table中,当然这个扩容不是线程安全的,后面剖析

7.get()方法解析


	  /**
	  * map.get()方法。
	  */
	  public V get(Object key) {
			//如果key=null默认就是table[0]位置的值
			if (key == null)
				return getForNullKey();
			//根据key查询entry
			Entry<K,V> entry = getEntry(key);
			//返回值,如果没有返回null
			return null == entry ? null : entry.getValue();
	  }
 /**
      * 返回与指定键关联的条目
      * 哈希映射。 如果 HashMap 不包含映射,则返回 null
      * 为钥匙。
      */
     final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
		//计算hash值
        int hash = (key == null) ? 0 : hash(key);
		//遍历当前位置的链表,直到找到对应key的值
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
			//判断哈希值,key是否相同,相同返沪当前的节点值e
			//== 比较的是内存地址;equals比较的是内存中的值是否相同
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }
 /**
      * 卸载版本的 get() 以查找空键。 空键映射
      * 到索引 0。这个空情况被拆分成单独的方法
      * 出于性能考虑,最常用的两种
      * 操作(get 和 put),但与条件合并
      * 其他。
      */
     private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

总结:这里陈述get()方法所执行的操作,根据key计算哈希值,计算存储的位置index,然后在拉链中或者单个桶的位置获取映射的值,如果找不到返回null

8.hashmap1.7高频面试题

8.1 什么是哈希?

Hash,一般翻译为“散列”,也有直接音译为“哈希”的, Hash就是指使用哈希算法是指把任意长度 的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。

8.2 什么是哈希冲突?

当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希 碰撞)。

8.3 如何解决哈希冲突?

hashmap中是采用链表法解决的。链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;

8.4 HashMap的长度为什么是2的幂次方?

  1. 为了能让 HashMap 存取高效,尽量减少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树 长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。

  2. 这个算法应该如何设计?

    我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的 前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效 率,这就解释了 HashMap 的长度为什么是2的幂次方。

  1. 在hash函数中为什么要用那么多的位运算和异或运算?

    这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置 的随机性&均匀性,最终减少Hash冲突,

8.5 能否使用任何类作为Map的key?

可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:

  1. 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。

  2. 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。

  3. 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。

  4. 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的 性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关 的问题了。

8.6 为什么HashMap中String、Integer这样的包装类适合作为K?

String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减 少Hash碰撞的几率

  • 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况

  • 内部已重写了 equals() 、 hashCode() 等方法,遵守了HashMap内部的规范(不清楚可以 去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

8.7 如果使用Object作为HashMap的Key,应该怎么办呢?

重写 hashCode() 和 equals() 方法

  1. 重写 hashCode() 是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中 排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;

  2. 重写 equals() 方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用 值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;

8.8 HashMap为什么不直接使用hashCode()处理后的哈希值直接作 为table的下标?

hashCode() 方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空 间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最 大值的,并且设备上也难以提供这么多的存储空间,从而导致通过 hashCode() 计算出的哈希值可 能不在数组大小范围内,进而无法匹配存储位置;

怎么解决?

  1. HashMap自己实现了自己的 hash() 方法,通过扰动运算使得它自己的哈希值高低位进行异或运算,降低哈希碰撞概率,也使得数据分布更加均匀

  2. 在保证数组长度为2的幂次方的时候,使用 hash() 运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有 当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;

8.9 HashMap 与 HashTable 有什么区别?

  1. 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本 都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap );

  2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被 淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用 ConcurrentHashMap );

  3. 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有 一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛 NullPointerException。

  4. 初始容量大小和每次扩充容量大小的不同 :

  5. 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来 的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。

  6. 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其 扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为 什么是2的幂次方。

9.hashmap1.7为什么线程不安全

hashmap1.7之所以线程不安全是发生在table扩容时,因为多个线程操作同一个table,所以就会发生多个线程共享同一个内存的线程安全的问题,在哪里会发生线程不安全呢?

我们看一下扩容迁移元素的代码

	 /**
      * 将所有条目从当前表转移到新表。
      */
    void transfer(Entry[] newTable, boolean rehash) {
		//新table的容量
        int newCapacity = newTable.length;
		//遍历旧的table,取出每一个元素
        for (Entry<K,V> e : table) {
			//这里用死循环,因为hashmap是数组+链表,所以这一步就是取出当前拉链上的每一个值
            while(null != e) {
				//!!!!!这里可能导致线程不安全也就是hashmap1.7循环链表的问题,后续讲解
				//next下一个节点
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
				//重新计算桶的位置
                int i = indexFor(e.hash, newCapacity);
				//把e.next指向newTable[i]的位置
                e.next = newTable[i];
				//把e放在i位置上,这里其实就是向下移动的一个操作,后面画图更好理解
                newTable[i] = e;
				//把next赋值给e,继续循环,直到  while(null != e),也就是当前桶的链表的最后一个位置被取出为止
                e = next;
            }
        }
    }

 比如线程1和线程2执行到如下代码时,线程二挂起,线程一进行迁移

Entry<K,V> next = e.next;

形成循环链表的图解如下(也就是hashmap1.7的循环链表问题)

自己看一下这个图,你就会对1.7产生循环链表的原因会非常清晰

好了,后续再补充,今天先到这 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值