`
天使的左手
  • 浏览: 55893 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

hashmap分析

    博客分类:
  • java
阅读更多
 
/**
hashmap底层维护着一个entry数组,每个数组索引指向的是一个entry链表.entry是一个key和value对,
entry内部还保存着一个next属性,用于指向下一个entry,如果entry后面没有其他的entry,则next=null
hashmap根据要存储entry中key的hashCode值,调用内部的hash()算法,返回一个hash值,然后跟
entry数组容量(2的倍数)capacity-1做按位与运算,来获得数组上的索引.这样的操作也可以称为碰撞检测.
执行add entry时,如果发现某个索引位置上已经存在有entry了,
则让新的entry的next指向当前数组索引指向的entry, 同时让当前数组索引指向新的entry.

关于hash值和capacity-1做按位与运算
因为entry数组的capacity大小都是2的倍数,所以用capacity-1和任何数做按位与操作后的大小都不会大于
capacity(>=0, <=capacity-1)
比如capacity=16,对应的二进制为10000
capacity-1:    1111
hash value:  110000 
------------------------- 
             000000 (最小0)
                         
capacity-1:    1111
hash value:  111111 
------------------------- 
             001111 (最大15)
任何数跟16-1做按位与后的结果,都不可能大于16,最大只能为16-1,最小为0
刚好在entry数组的索引范围之内.

hashmap源码分析
------------------------------------------------------------
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //初始化capacity不能超过MAXIMUM_CAPACITY
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //负载因子不能小于0或者为非数字
    //负载因子用于计算threshold,即当前entry数组最大能存储的entry数
    //如果超过threshold,就需要执行rehash操作(将原来的entry数组扩容2倍,得到一个新的entry数组,
    //遍历原entry数组,取出每个entry的hash值,
    //跟新数组的capacity-1做按位与操作得到新数组索引位置,存入entry)
    //负载因子越大,占用的存储空间越小,但是查询时间会相应的增加(碰撞检测的次数增加了),
    //因此效率会相应下降,负载因为越小,占用的存储空间会变大,查询速度也会相应的变快.
    //hashmap里面默认的负载因子大小为0.75
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    //得到一个大于等于初始capacity的最小2的倍数的数
    while (capacity < initialCapacity)
        capacity <<= 1; //每次左移一位,即capacity*2

    this.loadFactor = loadFactor;
    //threshold为数组capacity*负载因子
    threshold = (int)(capacity * loadFactor);
    //生成一个大小为capacity的entry数组
    table = new Entry[capacity];
    init();
}

public HashMap(int initialCapacity) {
    //传入自定义的initial capacity和默认的load factor
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    //默认的数组capacity为16
    table = new Entry[DEFAULT_INITIAL_CAPACITY];
    init();
}
--------------------------------------------
public V put(K key, V value) {
    if (key == null)
    	//key为null通过putForNullKey存入entry
        return putForNullKey(value);
    //调用hash算法用传入key的hashCode生成一个hash值
    int hash = hash(key.hashCode());
    //用hash值跟capacity-1做按位与,得到数组的索引位置
    int i = indexFor(hash, table.length);
    //遍历该索引上的entry链表
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //如果发现相同的key,用新的value替换掉原来的,将原来的value返回
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    //没找到key一样的entry,就新创建一个entry
    addEntry(hash, key, value, i);
    return null;
}

//hashmap里hash算法实现
static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

//hash值和entry数组capacity-1做按位与运算,返回运算结果
static int indexFor(int h, int length) {
    return h & (length-1);
}

void addEntry(int hash, K key, V value, int bucketIndex) {
    //获得当前数组索引上的entry
    Entry<K,V> e = table[bucketIndex];
    //创建一个新的entry,next指向数组索引指向的entry,然后数组索引指向新的entry
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    //如果当前已存入的entry数超过最大值threshold,执行rehash操作
    if (size++ >= threshold)
        resize(2 * table.length);
}
---------------------------------------------------
//操作跟put类似
public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}

----------------------------------------------------------------------
多线程环境下调用hashmap的get方法导致死循环
假设当前hashmap的capacity=2,hash()算法为key mod capacity
依次向hashmap存入{key=5,value=B},{key=3,value=A},{key=7,value=C}
因为key=3,5,7和capacity=2进行模运算结果都为1, 这三个entry都会出现在数组索引为1的地方,
且key=5的entry出现在链表的最后一个位置
hashmap底层数据结构如下图所示
     _ _
    |_0_|      _ _        _ _        _ _
    |_1_| --> | 7 |  --> | 3 |  --> | 5 | --> null
      ↓       |_C_|      |_A_|      |_B_| 
     entry数组                         ↓     
     (索引指向一个entry链表)           表示一个entry
                                    (每个entry都有一个next属性,
                                    指向下一个entry,
                                    最后一个entry的next为null)

假设向hashmap存入新的ertry时,已存入的entry数量(size)>=entry数组最大能存储的数量(threshold),
需要进行rehash操作,将entry数组扩容为原来大小的两倍,得到一个新的entry数组,遍历原数组,
取出每个索引值上的entry,如果entry不为空,拿到当前entry的hash值,跟新entry数组capacity-1做hash操作,
得到当前entry在新数组中的索引值,将当前entry的next设置为新entry数组所在索引指向的对象,
让新entry数组的所在索引指向当前entry对象(即[7,C]的next指向null, 
新entry数组索引为3的位置指向[7,C]),让当前entry的next作为当前entry,继续上面的操作,
直到遍历完原entry数组,最后将新的entry数组赋给原entry数组引用.

下图为rehash后的结果
     _ _       _ _
    |_0_|     | 5 | --> null
    |_1_| --> |_B_|
    |_2_|      _ _       _ _
    |_3_| --> | 3 | --> | 7 | --> null
              |_A_|     |_C_|

rehash操作源码
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    //生成一个新的entry数组,
    //resize(2 * table.length)
    //newCapacity为原entry数组长度的2倍 
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}        		   				 

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        //获得entry数组中索引j指向的entry
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                //保留当前entry的next 
                Entry<K,V> next = e.next; ①
                //取出当前entry的hash和newCapcacity-1做hash操作,
                //得到当前entry在新entry数组中的索引
                int i = indexFor(e.hash, newCapacity);
                //将当前entry的next设置为新entry数组所在索引指向的对象
                e.next = newTable[i];
                //新entry数组所在索引指向当前entry对象
                newTable[i] = e;
                //当前entry的next作为当前对象
                e = next;
            } while (e != null);
        }
    }
}

在单线程环境下,rehash不会有问题,而在多线程环境下,rehash就会有问题了
假设有两个线程A和B同时进入transfer方法执行rehash操作,线程A执行到①就被挂起了,
线程B一直到执行结束,线程A才重新获得调度,继续往下执行

当线程A被挂起的时候,e=[7,C], next=[3,A]
     _ _
    |_0_|      _ _        _ _        _ _
    |_1_| --> | 7 |  --> | 3 |  --> | 5 | --> null
              |_C_|      |_A_|      |_B_|

线程B执行完rehash后,entry table的结构为
     _ _       _ _
    |_0_|     | 5 | --> null
    |_1_| --> |_B_|
    |_2_|      _ _       _ _
    |_3_| --> | 3 | --> | 7 | --> null
              |_A_|     |_C_|
              
             
此时线程A是基于新的entry链表来rehash
    1) e=[7,C], next=[3,A] 挂起之前的值
     _ _ 
    |_0_|
    |_1_|
    |_2_|      _ _
    |_3_| --> | 7 | --> null
              |_C_| 
               
    2) e=[3,A], next=[7,C] 线程B执行rehash后[3,A]的next为[7,C]
     _ _ 
    |_0_|
    |_1_|
    |_2_|      _ _       _ _
    |_3_| --> | 3 | --> | 7 | --> null
              |_A_|     |_C_|             

    3) e=[7,C], next=null 线程B执行rehash后[7,C]的next为null
     _ _ 
    |_0_|
    |_1_|
    |_2_|      _ _       _ _
    |_3_| --> | 7 | --> | 3 | --> null
              |_C_| <-- |_A_|     
              
    此时[3,A]和[7,C]形成了循环引用
    此时再调用get(5)就会导致死循环出现                                  
*/
分享到:
评论

相关推荐

    HashMap 分析

    在本分析中,我们将会详细探讨HashMap在不同负载因子(loadFactor)、循环次数(loop)、哈希表长度(maptablelen)和映射长度(maplen)等条件下的行为和特性。 负载因子(loadFactor)是HashMap中的一个关键参数...

    HashMap分析

    JDK HashMap源码粗略分析 根据10个问题进行阅读探讨

    java HashMap原理分析

    Java HashMap原理分析 Java HashMap是一种基于哈希表的数据结构,它的存储原理是通过将Key-Value对存储在一个数组中,每个数组元素是一个链表,链表中的每个元素是一个Entry对象,Entry对象包含了Key、Value和指向...

    hashMap存储分析

    hashMap存储分析hashMap存储分析

    HashMap源码分析

    HashMap 是 Java 中最常用的集合类之一,它是基于哈希表实现的,提供了高效的数据存取功能。HashMap 的核心原理是将键(key)通过哈希函数转化为数组索引,然后将键值对(key-value pair)存储在数组的对应位置。...

    hashmap实现原理

    哈希映射(HashMap)是Java编程语言中广泛使用的数据结构之一,主要提供键值对的存储和查找功能。HashMap的实现基于哈希表的概念,它通过计算对象的哈希码来快速定位数据,从而实现了O(1)的平均时间复杂度。在深入...

    hashmap面试题_hashmap_

    面试中,可能会被问及HashMap的性能优化、内存占用分析、以及在特定场景下的选择,如并发环境、内存敏感的应用等。 总结,HashMap是Java编程中的基础工具,掌握其工作原理和常见面试题,不仅能帮助我们应对面试,更...

    面试必考之HashMap源码分析与实现

    面试中,HashMap的源码分析与实现是一个常见的考察点,因为它涉及到数据结构、并发处理和性能优化等多个核心领域。本篇文章将深入探讨HashMap的内部工作原理、关键特性以及其在实际应用中的考量因素。 HashMap基于...

    重载toString实现JS HashMap分析

    用过Java的都知道,里面有个功能强大的数据结构——HashMap,它能提供键与值的对应访问。不过熟悉JS的朋友也会说,JS里面到处都是hashmap,因为每个对象都提供了map[key]的访问形式。

    HashMap导致CPU100% 的分析

    HashMap导致CPU100% 的分析

    HashMap源码分析-思维导图.xmind

    hashmap的原理啊思想。

    HashMap与HashTable的区别(含源码分析)

    在Java编程语言中,`HashMap`和`HashTable`都是实现键值对存储的数据结构,但它们之间存在一些显著的区别,这些区别主要体现在线程安全性、性能、null值处理以及一些方法特性上。以下是对这两个类的详细分析: 1. ...

    HashMap类.rar

    通过分析源码,开发者可以深入理解哈希表的工作原理,学习如何在易语言中实现高效的数据结构,这对于提升程序性能和优化内存管理至关重要。同时,这也为自定义数据结构或实现其他哈希表相关的功能提供了基础。

    比较分析Vector、ArrayList和hashtable hashmap数据结构

    比较分析Vector、ArrayList和hashtable hashmap数据结构

    hashMap基本工作原理,图解分析,基础Map集合

    hashMap基本工作原理,图解分析,基础Map集合

    1.面试必考之HashMap源码分析与实现 伸缩性角度看HashMap的不足

    1.面试必考之HashMap源码分析与实现 伸缩性角度看HashMap的不足

Global site tag (gtag.js) - Google Analytics