美文网首页
HashMap相关

HashMap相关

作者: wuhuaguo丶 | 来源:发表于2019-02-11 21:34 被阅读0次

HashMap的底层实现原理,get的过程发生了什么

HashMap是用来存储key-value键值对的集合,每一个键值对叫做一个Entry,这些Entry分散存储在一个数组之中,这个数组就是HashMap的主干。
get方法:

public V get(Object key) {
     //如果key为null,则直接去table[0]处去检索即可。
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
 }

get方法通过key值返回对应的value,如果key为null,直接去table[0]处检索。

getEntry方法:

final Entry<K,V> getEntry(Object key) {
            
        if (size == 0) {
            return null;
        }
        //通过key的hashcode值计算hash值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor (hash&length-1) 获取最终数组索引,然后遍历链表,通过equals方法比对找出对应记录
        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 != null && key.equals(k))))
                return e;
        }
        return null;
    }

key(hashcode)-->hash-->indexFor-->最终索引位置,找到对应位置table[i],再查看是否有链表,遍历链表,通过key的equals方法比对查找对应的记录。

  • hashcode方法:当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了,说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。

  • 不同的对象可能会生成相同的hashcode值。虽然不能根据hashcode值判断两个对象是否相等,但是可以直接根据hashcode值判断两个对象不等,如果两个对象的hashcode值不等,则必定是两个不同的对象。如果要判断两个对象是否真正相等,必须通过equals方法。
    就是说对于两个对象,如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等;
    如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同;
    如果两个对象的hashcode值不等,则equals方法得到的结果必定为false;
    如果两个对象的hashcode值相等,则equals方法得到的结果未知。

  • 重写equals()方法的同时也要重写hashCode()方法:
    hashCode方法默认将对象的存储地址进行映射。所以,如果重写了equals()方法,同时要重写hashCode()方法。

  • Hash方法的规定:
    1.在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
    2.如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
    3.如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。
    假设两个对象,重写了其equals方法,其相等条件是属性相等,就返回true。如果不重写hashcode方法,其返回的依然是两个对象的内存地址值,必然不相等。这就出现了equals方法相等,但是hashcode不相等的情况。这不符合hashcode的规则。
    参与equals函数的字段,也必须都参与hashCode 的计算

package com.cxh.test1;
 
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
 
 
class People{
    private String name;
    private int age;
     
    public People(String name,int age) {
        this.name = name;
        this.age = age;
    }  
     
    public void setAge(int age){
        this.age = age;
    }
     
    @Override
    public int hashCode() {
        // TODO Auto-generated method stub
        return name.hashCode()*37+age;
    }
     
    @Override
    public boolean equals(Object obj) {
        // TODO Auto-generated method stub
        return this.name.equals(((People)obj).name) && this.age== ((People)obj).age;
    }
}
 
public class Main {
 
    public static void main(String[] args) {
         
        People p1 = new People("Jack", 12);
        System.out.println(p1.hashCode());
             
        HashMap<People, Integer> hashMap = new HashMap<People, Integer>();
        hashMap.put(p1, 1);
         
        System.out.println(hashMap.get(new People("Jack", 12)));
    }
}

输出为1。

  • 在程序执行期间,只要equals方法的比较操作用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法必须始终如一地返回同一个整数。
  • 如果两个对象根据equals方法比较是相等的,那么调用两个对象的hashCode方法必须返回相同的整数结果。
  • 如果两个对象根据equals方法比较是不等的,则hashCode方法不一定得返回不同的整数。
  • 设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该产生同样的值。如果在讲一个对象用put()添加进HashMap时产生一个hashCdoe值,而用get()取出时却产生了另一个hashCode值,那么就无法获取该对象了。所以如果你的hashCode方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,hashCode()方法就会生成一个不同的散列码。

HashMap原理深入理解


HashMap与Hashtable的区别

1、Hashtable线程安全,速度较慢。它为每一个方法都加了Synchronize方法。在多线程环境下,可以直接使用Hashtable。HashMap不是线程安全的,速度较快,在使用时可能会产生死锁问题。在多线程环境下使用要自己加入同步方法。
2、计算hash值的方法不同。
Hashtable直接使用对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数发来获得最终的位置。


hashtable计算hash方法

Hashtable在计算元素的位置时需要进行一次除法运算,而除法运算是比较耗时的。
HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。
HashMap的效率虽然提高了,但是hash冲突却也增加了。因为它得出的hash值的低位相同的概率比较高。
为了解决这个问题,HashMap重新根据hashcode计算hash值后,又对hash值做了一些运算来打散数据。使得取得的位置更加分散,从而减少了hash冲突。当然了,为了高效,HashMap只做了一些简单的位处理。从而不至于把使用2 的幂次方带来的效率提升给抵消掉。


hashmap计算hash方法
3、Hashtable中,key与value都不允许出现null。在 HashMap 中, null 可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为 null 。当 get() 方法返回 null 值时,即可以表示 HashMap 中没有该键,也可以表示该键所对应的值为 null 。因此,在 HashMap 中不能由 get() 方法来判断 HashMap 中是否存在某个键, 而应该用 containsKey() 方法来判断。

HashMap能否实现同步

可以,可以使用ConcurrentHashMap,或者
Map m = Collections.synchronizeMap(hashMap);

API

HashMap实现原理

  • JDK1.8之前:数组+链表
  • JDK1.8及之后:数组+链表,当链表长度大于阈值(默认为8)时,将链表转变为红黑树,以减少搜索时间。TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
    java7中 hashMap每个桶中放置的是链表,这样当hash碰撞严重时,会导致个别位置链表长度过长,从而影响性能。
    java8中,HashMap 每个桶中当链表长度超过8之后,会将链表转换成红黑树,从而提升增删改查的速度。

    详细内容参照 GitHub

Java集合框架面试题几乎必问


ConcurrentHashMap与HashTable的区别

主要体现在实现线程安全的方式的不同

  • 底层数据结构不同:1.7之前的ConcurrentHashMap使用的是数组加链表,1.8之后的ConcurrentHashMap使用的和1.8的HashMap一样,在阈值大于8时,转化为红黑树。
  • 实现线程安全的方式不同:
    HashTable(同一把锁):使用 synchronized来保证线程安全,效率很低,当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
    ConcurrentHashMap :1.7的时候,使用分段锁,对整个桶数组进行分割分段(Segment),每一把锁只锁容器其中一部分数据。多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
    到了1.8的时候,已经摒弃了segment的概念,直接用Node数组+链表+红黑树的数据结构来实现,并发使用synchronized和CAS来操作。整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。
    详细内容参照 GitHub

Java集合框架面试题几乎必问


高并发下HashMap容易出现的问题

多线程并发扩容的时候容易发生死循环。
HashMap并不是线程安全,所以在多线程情况下,应该首先考虑用ConcurrentHashMap。
在高并发的情况下,是非常可能发生死循环的,由此造成CPU 100%,这是非常可怕的。
原因简单来说:如果HashMap到达临界容量需要扩容,两个线程同时进行resize操作,同时开辟两块空间,线程挂起时机不当时,rehash会产生环路。


多线程HashMap

当调用Get查找一个不存在的Key,而这个Key的Hash结果恰好等于3的时候,由于位置3带有环形链表,所以程序将会进入死循环!


为什么数组长度要保证为2的幂次方?

在确定哈希桶数组索引位置的时候,有一步操作是用元素的hash值与HashMap的长度-1 做“与”运算,对于新插入的数据或者待读取的数据,HashMap将Key的哈希值对数组长度取模,结果作为该Entry在数组中的index。在计算机中,取模的代价远高于位操作的代价,因此HashMap要求数组的长度必须为2的N次方。

static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
return h & (length-1); //第三步 取模运算
}

当数组长度为2的幂次的时候,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;
如果 length 为 2 的次幂 则 length-1 转化为二进制必定是 11111……的形式,在与 h 的二进制与操作效率会非常的快,而且空间不浪费;如果 length 不是 2 的次幂,比如 length 为 15,则 length - 1 为 14,对应的二进制为 1110,在于 h 与操作,最后一位都为 0 ,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。
一招破解 Java 集合类面试题


ConcurrentHashMap在JDK1.7和JDK1.8中的实现

Java7中的ConcurrentHashMap的底层数据结构仍为数组+链表,但与HashMap不同的是, ConcurrentHashMap最外层不是大数组,而是一个Segment数组,每个segment数组包含于HashMap数据结构差不多的链表数组。整体数据结构如下:


寻址方式:
在读写某个key时,先取该key的hash值,并将哈希值的高N位对Segment个数取模从而得到该key应该属于哪个Segment,接着如同操作HashMap一样操作Segment。
同步方式:
Segment继承自ReentrantLock,是一种可重入锁,所以我们可以很方便的对每一个Segment上锁。
对于读操作,获取Key所在的Segment时,需要保证可见性。具体实现上可以使用volatile关键字,也可使用锁。但使用锁开销太大,而使用volatile时每次写操作都会让所有CPU内缓存无效,也有一定开销。ConcurrentHashMap使用如下方法保证可见性,取得最新的Segment。
Segment<K,V> s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)
对于写操作:并不要求在写的时候把所有的Segment都锁住,那样相当于锁住了整个map,它会先获取这个key对应的Segment的锁,获取成功后就可以像操作普通HashMap一样操作该Segment,并保证Segment的安全性,同时其他的Segment并没有被锁住,因此理论上可以支持Segment个数的线程同时安全的并发读写。

获取锁时,并不直接使用lock获取,因为该方法在失败的时候会挂起,事实上,他是用了自旋锁,如果trylock失败,就会通过循环再次尝试trylock,如果次数超过一定限制,就转化为lock互斥锁。

有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁

以上仅针对Java7有效


在Java8中,为进一步提高并发性,摒弃了Segment的概念,和HashMap一样,在链表长度小于阈值的时候,使用数组+链表,超过阈值之后,转化为红黑树。
同步方式:put操作,如果key对应的数组元素为null,则通过CAS将其设置为当前值,如果key对应的数组元素(链表表头或者树根)不为null,则对该元素使用synchronized关键字申请锁,然后进行操作,如果该put操作使得当前链表长度超过一定阈值,则将该链表转换为树,从而提高寻址效率。

对于读操作,由于数组被volatile关键字修饰,因此不用担心数组的可见性问题。同时每个元素是一个Node实例(Java 7中每个元素是一个HashEntry),它的Key值和hash值都由final修饰,不可变更,无须关心它们被修改后的可见性问题。而其Value及对下一个元素的引用由volatile修饰,可见性也有保障。

size操作:
put方法和remove方法都会通过addCount方法维护Map的size。size方法通过sumCount获取由addCount方法维护的Map的size。


散列表处理hash冲突的方法

  • 开放定址法
    ThreadLocalMap
    如果发生冲突,算法会简单的从该槽位置向后循环遍历hash表,直到找到表中的下一个空槽,并将该元素放入该槽中(会导致相同hash值的元素挨在一起和其他hash值对应的槽被占用)
  • 链地址法
    hashmap
    拉链法的优点
    与开放定址法相比,拉链法有如下几个优点:
    ①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
    ②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
    ③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
    ④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
  • 再散列法
    当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。缺点:计算时间增加。

相关文章

  • HashMap相关

    HashMap容量为2次幂的原因 hash方法可以计算一个对象的hashcode,我们不用过于关注 但是他计算ha...

  • HashMap相关

    HashMap是数组+链表 1.HashMap不是线程安全,为什么不是线程安全的呢? 多线程put,多线程reha...

  • HashMap相关

    HashMap、LinkedHashMap、ConcurrentHashMap、ArrayMap、SparseMa...

  • HashMap相关

    hash概念 hash:是一种信息摘要算法,它还叫做哈希,或者散列。我们平时使用的MD5中的公私钥验证都属于Has...

  • HashMap相关

    https://www.jianshu.com/p/45fa4e80b631HashMap与HashTable都实...

  • HashMap相关

    HashMap的底层实现原理,get的过程发生了什么 HashMap是用来存储key-value键值对的集合,每一...

  • ConcurrentHashMap原理分析

    相关的HashMap java里面有HashMap、HashTable 和 ConcurrentHashMap。其...

  • JDK1.8中HashMap底层实现原理

    接下来会从以下几个方面介绍 HashMap 源码相关知识: 1、HashMap 存储结构 2、HashMap 各常...

  • hashmap相关问题

    概述 java8对java7中hashmap的实现进行了一部分修改,最大的修改在于利用了红黑树,jdk7使用数组+...

  • java源码分析之HashMap(三)

    相关文章java源码分析之HashMap(一)java源码分析之HashMap(二)https://blog.cs...

网友评论

      本文标题:HashMap相关

      本文链接:https://www.haomeiwen.com/subject/pepdjqtx.html