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

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

Java中的HashMap源码记录以及并发环境的几个问题的更多相关文章

  1. Java中关于HashMap源码的研究

    1.基础知识 1.数组 数组存储区间是连续的,占用内存严重,故空间复杂的很大.但数组的二分查找时间复杂度小,为O(1):数组的特点是:寻址容易,插入和删除困难. 2.链表 链表存储区间离散,占用内存比 ...

  2. Java HashSet和HashMap源码剖析

    转自: Java HashSet和HashMap源码剖析 总体介绍 之所以把HashSet和HashMap放在一起讲解,是因为二者在Java里有着相同的实现,前者仅仅是对后者做了一层包装,也就是说Ha ...

  3. 【转】Java集合:HashMap源码剖析

    Java集合:HashMap源码剖析   一.HashMap概述二.HashMap的数据结构三.HashMap源码分析     1.关键属性     2.构造方法     3.存储数据     4.调 ...

  4. 死磕Java之聊聊HashMap源码(基于JDK1.8)

    死磕Java之聊聊HashMap源码(基于JDK1.8) http://cmsblogs.com/?p=4731 为什么面试要问hashmap 的原理

  5. 死磕 java集合之HashMap源码分析

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 简介 HashMap采用key/value存储结构,每个key对应唯一的value,查询和修改 ...

  6. 【JAVA集合】HashMap源码分析(转载)

    原文出处:http://www.cnblogs.com/chenpi/p/5280304.html 以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储 ...

  7. Java集合:HashMap源码剖析

    一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap  ...

  8. JDK8中的HashMap源码

    背景 很久以前看过源码,但是猛一看总感觉挺难的,很少看下去.当时总感觉是水平不到.工作中也遇到一些想看源码的地方,但是遇到写的复杂些的心里就打退堂鼓了. 最近在接手同事的代码时,有一些很长的pytho ...

  9. java集合中的HashMap源码分析

    1.hashMap中的成员分析 transient Node<K,V>[] table; //为hash桶的数量 /** * The number of key-value mapping ...

随机推荐

  1. 阿里云 Ubuntu16.04 apache2 ssl证书下载与安装(必须有域名)

    阿里云申请免费SSL证书并下载(包含xxx.key|xxx._root_bundle.crt|xxx._public.crt三个文件) 用https是自己的网站收到保护,不易被攻克,所以保护自己的网站 ...

  2. Jmeter获取接口返回数组的长度

    添加BeanShell PostProcessor来获取,具体代码如下: import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath ...

  3. 数据库MongoDB

    一.MongoDB简介 MongoDB是由c++语言编写的,是一个基于分布式文件存储的开源数据库系统,在高负载的情况下,添加更多的节点,可以保证服务器性能.MongoDB旨在为web应用提供扩展的高性 ...

  4. python中的函数对象的内存地址是多少

    今天和同学讨论一个问题,发现了函数的内存地址和我想象的不一样. 我以为同一个函数,假如给的参数不一样,那么这两个函数的id就不一样. 然后经过实验,发现python为了便于管理函数,所有的函数都放在同 ...

  5. win10 uwp 如何开始写 uwp 程序

    本文告诉大家如何创建一个 UWP 程序. 这是一系列的 uwp 入门博客,所以写的很简单 本文来告诉大家如何创建一个简单的程序 安装 VisualStudio 在开始写 UWP 需要安装 Visual ...

  6. JS 上传图片 + 预览功能(一)

    JS 上传图片 + 预览功能 <body> <input type="file" id="fileimg1" style="disp ...

  7. Harbor 搜索镜像及查看 tag

    在我们搭建完 Harbor 后: https://www.cnblogs.com/klvchen/p/9482153.html 如果想要通过 API 获取 Harbor 上面的镜像及 tag 可以使用 ...

  8. js array 数组添加与删除数据

    //新建数组 var ids =new Array(); //向数组中添加数据 ids.push(1); //删除下标为i的数组元素 ids.remove(i); //自定义删除函数: Array.p ...

  9. React中使用styled-components的基础使用

    今天准备来给大家分享分享React中styled-components的基础使用,仅仅是我个人的一些理解,不一定全对,有错误还请大佬们指出,496838236这是我qq,有想指点我的大佬随时加我qq好 ...

  10. 用css实现圆形波浪效果图

    在移动端经常看到一些圆形波浪图来显示金额,刚开始我认为这种效果只能用canvas写的,后来发现用css也可以. 原理:我们都知道让块元素的border-radius:50%会变成圆形,如果少于50%呢 ...