Java HashMap源码及并发环境常见问题解决

HashMap源码简单分析:

1 一切需要从HashMap属性字段说起:

/** The default initial capacity - MUST be a power of two. 初始容量 */
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 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. 最大容量
   */
  static final int MAXIMUM_CAPACITY = 1 << 30;

  /**
   * The load factor used when none specified in constructor.    * 默认的负载因子,当map的size>=负载因子*capacity时候并且插入元素时候的table[i]!=null进行扩容   * 扩容判断逻辑:java.util.HashMap#addEntry函数中   *
   */
  static final float DEFAULT_LOAD_FACTOR = 0.75f;

  /**
   * An empty table instance to share when the table is not inflated.
   */
  static final Entry<?,?>[] EMPTY_TABLE = {};

  /**
   * The table, resized as necessary. Length MUST Always be a power of two. 哈希表
   */
  transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

  /**
   * The number of key-value mappings contained in this map. map的大小
   */
  transient int size;

  /**
   * The next size value at which to resize (capacity * load factor).
   * @serial
   */
  // If table == EMPTY_TABLE then this is the initial capacity at which the
  // table will be created when inflated. 扩容的阈值 = capacity * 负载因子
  int threshold;

  /**
   * The load factor for the hash table. 负载因子,默认是0.75,可以在创建HashMap时候通过构造函数指定
   *
   * @serial
   */
  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).   * 修改次数:例如进行rehash或者返回hashMap视图时候如果发生修改可以fast-fail
   */
  transient int modCount;

  /**
   * The default threshold of map capacity above which alternative hashing is
   * used for String keys. Alternative hashing reduces the incidence of
   * collisions due to weak hash code calculation for String keys.
   * <p/>
   * This value may be overridden by defining the system property
   * {@code jdk.map.althashing.threshold}. A property value of {@code 1}
   * forces alternative hashing to be used at all times whereas
   * {@code -1} value ensures that alternative hashing is never used.   * rehash时候判断的一个阈值
   */
  static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

2: 接下来查看一下HashMap的put方法:

/**
   * Associates the specified value with the specified key in this map.
   * If the map previously contained a mapping for the key, the old
   * value is replaced.
   *
   * @param key key with which the specified value is to be associated
   * @param value value to be associated with the specified key
   * @return the previous value associated with <tt>key</tt>, or
   *     <tt>null</tt> if there was no mapping for <tt>key</tt>.
   *     (A <tt>null</tt> return can also indicate that the map
   *     previously associated <tt>null</tt> with <tt>key</tt>.)
   */
  public V put(K key, V value) {
    if (table == EMPTY_TABLE) {//初始化哈希表
      inflateTable(threshold);
    }
    if (key == null) //如果key 为null 存储到table[0]位置
      return putForNullKey(value);
    int hash = hash(key); //计算hash值
    int i = indexFor(hash, table.length);//计算entry在table中的位置
    //for循环逻辑用于修改key对应的value的
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
      Object k;
      if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        V oldValue = e.value;
        e.value = value;
        e.recordAccess(this);
        return oldValue;//如果是更新返回旧值
      }
    }
    //修改次数++
    modCount++;
    //添加元素到哈希表中
    addEntry(hash, key, value, i);
    // 如果是添加元素则返回null
    return null;
  }

3 put中调用的inflateTable方法:

/**
   * Inflates the table.
   */
  private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    //计算大于等于toSize的最小的2的整数次幂的值
    int capacity = roundUpToPowerOf2(toSize);
    //计算扩容阈值
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //初始化哈希表
    table = new Entry[capacity];
    //更新一下rehash的判断条件,便于以后判断是否rehash
    initHashSeedAsNeeded(capacity);
  }

4 put方法中调用的indexFor方法:

/**
   * Returns index for hash code h. 返回哈希值对应的哈希表索引
   */
  static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
   //使用&操作,而不使用取余原因:均匀分布在哈希表中 。length-1目的是:由于table的长度都是2的整数次幂进行扩容,length-1的二进制全是1,计算效率高
    return h & (length-1);
  }

5 put方法中调用的addEntry方法:

/**
   * Adds a new entry with the specified key, value and hash code to
   * the specified bucket. It is the responsibility of this
   * method to resize the table if appropriate.
   *
   * Subclass overrides this to alter the behavior of put method.
   */
  void addEntry(int hash, K key, V value, int bucketIndex) {
   //判断是否扩容,只有size大于等于阈值而且当前插入table[i]!=null(就是able[i]已经被占用则扩容)
   if ((size >= threshold) && (null != table[bucketIndex])) {
      resize(2 * table.length);
      hash = (null != key) ? hash(key) : 0;
      //如果需要扩容的话则需要更新再次重新计算哈希表位置
      bucketIndex = indexFor(hash, table.length);
    }
    //将值插入到哈希表中
    createEntry(hash, key, value, bucketIndex);
  }

6 addEntry方法中调用的createEntry方法:

/**
   * Like addEntry except that this version is used when creating entries
   * as part of Map construction or "pseudo-construction" (cloning,
   * deserialization). This version needn't worry about resizing the table.
   *
   * Subclass overrides this to alter the behavior of HashMap(Map),
   * clone, and readObject.
   */
  void createEntry(int hash, K key, V value, int bucketIndex) {
    // 获取到哈希表指定位置
    Entry<K,V> e = table[bucketIndex];
    // 链表的头插入方式进行插入,插入逻辑在Entry的构造器中。然后将新节点存储到 table[bucketIndex]中
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;//更新size即可
  }

Entry构造器:

/**
   *
   * @param h hash值
   * @param k key
   * @param v value
   * @param n 原始链表
   */
  Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    //将原始链表接该节点后面
    next = n;
    key = k;
    hash = h;
  }

7 接下来看一下java.util.HashMap#addEntry扩容机制:

当进行扩容时候需要重新计算哈希值和在哈希表中的位置。

void addEntry(int hash, K key, V value, int bucketIndex) {
    //满足扩容条件进行扩容
    if ((size >= threshold) && (null != table[bucketIndex])) {
      //扩容,2倍进行扩容
      resize(2 * table.length);
      //重新计算哈数值
      hash = (null != key) ? hash(key) : 0;
      //重新计算哈希表中的位置
      bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
  }

接下来看一下java.util.HashMap#resize方法:

/**
   * Rehashes the contents of this map into a new array with a
   * larger capacity. This method is called automatically when the
   * number of keys in this map reaches its threshold.
   *
   * If current capacity is MAXIMUM_CAPACITY, this method does not
   * resize the map, but sets threshold to Integer.MAX_VALUE.
   * This has the effect of preventing future calls.
   *
   * @param newCapacity the new capacity, MUST be a power of two;
   *    must be greater than current capacity unless current
   *    capacity is MAXIMUM_CAPACITY (in which case value
   *    is irrelevant).
   */
  void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {//判断当前old容量是否最最大容量,是的话更新阈值
      threshold = Integer.MAX_VALUE;
      return;
    }
    //创建新的表
    Entry[] newTable = new Entry[newCapacity];
    //元素转移,根据initHashSeedAsNeeded结果判断是否进行rehash
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // 新表赋给table
    table = newTable;
    //更新阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  }

关于HashMap在并发情况下的常见问题,其实在多线程环境下使用HashMap本来就是有风险错误的,但是一般面试却喜欢这么问,下面列举一下自己印象中的常见问题:

1:在进行扩容时候,其他线程是否可以进行进行插入操作(多线程环境下可能会导致HashMap进入死循环,此处暂不考虑)?

答:首先HashMap就不是一个线程安全的容器,所以在多线程环境下使用就是错误的。其次在扩容时候可以进行插入的,但是不安全。例如:

当主线程在调用transfer方法进行复制元素:

/**
   * Transfers all entries from current table to newTable.
   */
  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) {
          e.hash = null == e.key ? 0 : hash(e.key);
        }
        int i = indexFor(e.hash, newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
      }
    }
  }

此时另一个线程在添加新元素是可以的,新元素添加到table中。如果子线程需要扩容的话可以进行扩容,然后将新容器赋给table。而此时主线程转移元素的工作就是将table中元素转移到newTable中。注意main线程的transfer方法:

如果main线程刚进入transfer方法时候newTable大小是32的话,由于子线程的添加操作导致table此时元素如果有128的话。则128个元素就会存储到大小为32的newTable中(此处不会扩容)。这就会导致HashMap性能下降!!!

可以使用多线程环境进行debug查看即可确定(推荐Idea的debug,的确强大,尤其是Evaluate Expression功能)。

2:进行扩容时候元素是否需要重新Hash?

这个需要具体情况判断,调用initHashSeedAsNeeded方法判断(判断逻辑这里先不介绍)。

/**
   * Rehashes the contents of this map into a new array with a
   * larger capacity. This method is called automatically when the
   * number of keys in this map reaches its threshold.
   *
   * If current capacity is MAXIMUM_CAPACITY, this method does not
   * resize the map, but sets threshold to Integer.MAX_VALUE.
   * This has the effect of preventing future calls.
   *
   * @param newCapacity the new capacity, MUST be a power of two;
   *    must be greater than current capacity unless current
   *    capacity is MAXIMUM_CAPACITY (in which case value
   *    is irrelevant).
   */
  void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
      threshold = Integer.MAX_VALUE;
      return;
    }

    Entry[] newTable = new Entry[newCapacity];
    //initHashSeedAsNeeded 判断是否需要重新Hash
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  }

然后进行转移元素:

/**
   * Transfers all entries from current table to newTable.
   */
  void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    //多线程环境下,如果其他线程导致table快速扩大。newTable在此处无法扩容会导致性能下降。但是如果后面有再次调用put方法的话可以再次触发resize。
    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);
        }
        int i = indexFor(e.hash, newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
      }
    }
  }

3:如何判断是否需要重新Hash?

/**
   * Initialize the hashing mask value. We defer initialization until we
   * really need it.
   */
  final boolean initHashSeedAsNeeded(int capacity) {

    // hashSeed降低hash碰撞的hash种子,初始值为0
    boolean currentAltHashing = hashSeed != 0;
    //ALTERNATIVE_HASHING_THRESHOLD: 当map的capacity容量大于这个值的时候并满足其他条件时候进行重新hash
    boolean useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    //TODO 异或操作,二者满足一个条件即可rehash
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {
      // 更新hashseed的值
      hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0;
    }
    return switching;
  }

4:HashMap在多线程环境下进行put操作如何导致的死循环?

死循环产生时机:

当两个线程同时需要进行扩容,而且对哈希表同一个桶(table[i])进行扩容时候,一个线程刚好确定e和next元素之后,线程被挂起。此时另一个线程得到cpu并顺利对该桶完成转移(需要要求被转移之后的线程1中的e和next指的元素在新哈希表的同一个桶中,此时e和next被逆序了)。接着线程从挂起恢复回来时候就会陷入死循环中。参考:https://coolshell.cn/articles/9606.html

产生原因:主要由于并发操作,对用一个桶的两个节点构成了环,导致对环进行无法转移完毕元素陷入死循环。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 学习Java HashMap,看这篇就够了

    HashMap 是一个散列表,它存储的内容是键值对(key-value)映射. HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步. HashMap 是无序的,即不会记录插入的顺序. HashMap 继承于AbstractMap,实现了 Map.Cloneable.java.io.Serializable 接口. HashMap 的 key 与 value 类型可以相同也可以不同,可以是字符串(Str

  • Java实现简易HashMap功能详解

    本文实例讲述了Java实现简易HashMap功能.分享给大家供大家参考,具体如下: 创建节点类 节点类含有的属性:键值对(value,key)以及指向下一节点的next: 这些属性的get以及set方法 代码如下: /** * 节点类 * @author HP * */ public class Node { private Object value; private Object key; private Node next; /** * 空节点 */ public Node() { } /*

  • Java手写简易版HashMap的使用(存储+查找)

    HashMap的基本结构 package com.liuyuhe; public class Node { int hash; Object key; Object value; Node next; } package com.liuyuhe; public class MyHashMap { Node[] table; //位桶数组 int size; //存放键值对的个数 public MyHashMap() { table=new Node[16]; } } put()方法存储键值对 p

  • Java8 HashMap扩容算法实例解析

    这篇文章主要介绍了Java8 HashMap扩容算法实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Java8的HashMap扩容过程主要就是集中在resize()方法中 final Node<K,V>[] resize() { // ...省略不重要的 } 其中,当HashMap扩容完毕之后,需要对原有的数据进行转移.因为容量变大了,部分元素的位置因此要变更,因而出现了下面的这个转移过程. 转移过程大致是:依次从旧数组里取值,然后从

  • Java中遍历ConcurrentHashMap的四种方式详解

    这篇文章主要介绍了Java中遍历ConcurrentHashMap的四种方式详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 方式一:在for-each循环中使用entries来遍历 System.out.println("方式一:在for-each循环中使用entries来遍历");for (Map.Entry<String, String> entry: map.entrySet()) { System.out.pr

  • JAVA--HashMap热门面试题

    1. 为什么我们建议在定义HashMap的时候,就指定它的初始化大小呢? 答:在当我们对HashMap初始化时,如果没有为其设置初始化容量,那么系统会默认创建一个容量为16的大小的集合.当我们向HashMap中添加元素时,如果HashMap的容量值超过了它的临界值(默认16*0.75=12)时,(0.75是HashMap的加载因子)HashMap将会重新扩容到下一个2的指数次幂(2^4=16 下一个2的指数次幂是2^5=32).由于HashMap扩容要进行resize的操作,频繁的resize,

  • java中hashmap容量的初始化实现

    HashMap使用HashMap(int initialCapacity)对集合进行初始化. 在默认的情况下,HashMap的容量是16.但是如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量.比如如果指定了3,则容量是4:如果指定了7,则容量是8:如果指定了9,则容量是16. 为什么要设置HashMap的初始化容量 在<阿里巴巴Java开发手册>中,有一条开发建议是建议我们设置HashMap的初始化容量. 下面我们通过具体的代码来了解下为什么会这么

  • JAVA中哈希表HashMap的深入学习

    深入浅出学Java--HashMap 哈希表(hash table) 也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,本文会对java集合框架中HashMap的实现原理进行讲解,并对JDK7的HashMap源码进行分析. 一.什么是哈希表 在讨论哈希表之前,我们先大概了解下其他数据结构在新增,查找等基础操作执行性能 数组:采用一段连续的存储单元来存储数据.对于指定下标的查找,时间复杂度为O(1):通过给定值进

  • Java HashMap源码及并发环境常见问题解决

    HashMap源码简单分析: 1 一切需要从HashMap属性字段说起: /** The default initial capacity - MUST be a power of two. 初始容量 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * The maximum capacity, used if a higher value is implicitly specified * by eit

  • Java HashMap源码深入分析讲解

    1.HashMap是数组+链表(红黑树)的数据结构. 数组用来存放HashMap的Key,链表.红黑树用来存放HashMap的value. 2.HashMap大小的确定: 1) HashMap的初始大小是16,在下面的源码分析中会看到. 2)如果创建时给定大小,HashMap会通过计算得到1.2.4.8.16.32.64....这样的二进制位作为HashMap数组的大小. //如何做到的呢?通过右移和或运算,最终n = xxx11111.n+1 = xx100000,2的n次方,即为数组大小 s

  • Java1.7全网最深入HashMap源码解析

    目录 存储结构 属性成员 构造函数: hash方法 Map中添加数据 put方法 流程图 源码 inflateTable方法 putForNullKey方法 addEntry方法 createEntry方法 扩容方法 resize方法 transfer方法 从HashMap中获取数据 get方法 从HashMap中删除数据 remove方法 对HashMap的其他操作 1.7和1.8版本区别 数据结构 hash值计算方式 扩容机制 存储结构 内部包含了一个 Entry 类型的数组 table.E

  • 深入理解Java之HashMap源码剖析

    一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap. Map map = Coll

  • Java集合系列之HashMap源码分析

    前面我们已经分析了ArrayList和LinkedList这两个集合,我们知道ArrayList是基于数组实现的,LinkedList是基于链表实现的.它们各自有自己的优劣势,例如ArrayList在定位查找元素时会优于LinkedList,而LinkedList在添加删除元素时会优于ArrayList.而本篇介绍的HashMap综合了二者的优势,它的底层是基于哈希表实现的,如果不考虑哈希冲突的话,HashMap在增删改查操作上的时间复杂度都能够达到惊人的O(1).我们先看看它所基于的哈希表的结

  • java String源码和String常量池的全面解析

    1. String 介绍,常用方法源码分析 2. String 常量池分析 常用方法 equals trim replace concat split startsWith 和 endsWith substring toUpperCase() 和 toLowerCase() compareTo String 介绍 String类被final所修饰,也就是说String对象是不可变量,并发程序最喜欢不可变量了.String类实现了Serializable, Comparable, CharSequ

  • java集合类源码分析之Set详解

    Set集合与List一样,都是继承自Collection接口,常用的实现类有HashSet和TreeSet.值得注意的是,HashSet是通过HashMap来实现的而TreeSet是通过TreeMap来实现的,所以HashSet和TreeSet都没有自己的数据结构,具体可以归纳如下: •Set集合中的元素不能重复,即元素唯一 •HashSet按元素的哈希值存储,所以是无序的,并且最多允许一个null对象 •TreeSet按元素的大小存储,所以是有序的,并且不允许null对象 •Set集合没有ge

  • Java集合源码全面分析

    Java集合工具包位于Java.util包下,包含了很多常用的数据结构,如数组.链表.栈.队列.集合.哈希表等.学习Java集合框架下大致可以分为如下五个部分:List列表.Set集合.Map映射.迭代器(Iterator.Enumeration).工具类(Arrays.Collections). 从上图中可以看出,集合类主要分为两大类:Collection和Map. Collection是List.Set等集合高度抽象出来的接口,它包含了这些集合的基本操作,它主要又分为两大部分:List和Se

  • Java CopyOnWriteArrayList源码超详细分析

    目录 一.概述 二.类图 三.核心方法 1.add() 2.set() 3.remove() 4.get() 5.size() 四.总结 一.概述 CopyOnWriteArrayList是基于写时复制技术实现的,适用于读多写少场景下的线程安全的并发容器.读操作永远不会加锁,读读.读写都不会冲突,只有写写需要等待.写操作时,为了不影响其它线程的读取,它会进行一次自我复制,待数据写入完成后再替换array数组.array数组是被volatile修饰的,它被修改后可以被其他线程立刻发现. publi

  • java TreeMap源码解析详解

    java TreeMap源码解析详解 在介绍TreeMap之前,我们来了解一种数据结构:排序二叉树.相信学过数据结构的同学知道,这种结构的数据存储形式在查找的时候效率非常高. 如图所示,这种数据结构是以二叉树为基础的,所有的左孩子的value值都是小于根结点的value值的,所有右孩子的value值都是大于根结点的.这样做的好处在于:如果需要按照键值查找数据元素,只要比较当前结点的value值即可(小于当前结点value值的,往左走,否则往右走),这种方式,每次可以减少一半的操作,所以效率比较高

随机推荐