zoukankan      html  css  js  c++  java
  • 什么是Hash?什么是Hash算法或哈希函数?什么是map?什么是HashMap?HashMap的实现原理或者工作原理?HashMap是线程安全的吗?为什么?如何解决?

    1、什么是Hash

    Hash也称散列、哈希,对应的英文都是Hash。基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值。

     

    2.什么是Hash算法或哈希函数?

    (1)Hash函数(Hash算法):

    在一般的线性表、树结构中,数据的存储位置是随机的,不像数组可以通过索引能一步查找到目标元素。为了能快速地在没有索引之类的结构中找到目标元素,需要为存储地址和值之间做一种映射关系h(key),这个h就是哈希函数,

    用公式表示:

    h(key)=Addr

    h:哈希函数

    key:关键字,用来唯一区分对象的

          把线性表中每个对象的关键字通过哈希函数h(key)映射到内存单元地址,并把对象存储到该内存单元,这样的线性表存储结构称为哈希表或散列表。

    (2)在设置哈希函数时,通常要考虑以下因素:

      ○ 计算函希函数所需的时间

      ○ 关键字的长度

      ○ 哈希表的长度

      ○ 关键字的分布情况

      ○ 记录的查找频率

    (3)Hash碰撞的解决方案

    链地址法

    链表地址法是使用一个链表数组,来存储相应数据,当hash遇到冲突的时候依次添加到链表的后面进行处理。

     

    链地址在处理的流程如下:
    添加一个元素的时候,首先计算元素key的hash值,确定插入数组中的位置。如果当前位置下没有重复数据,则直接添加到当前位置。当遇到冲突的时候,添加到同一个hash值的元素后面,行成一个链表。这个链表的特点是同一个链表上的Hash值相同。java的数据结构HashMap使用的就是这种方法来处理冲突,JDK1.8中,针对链表上的数据超过8条的时候,使用了红黑树进行优化。

    开放地址法

    开放地址法是指大小为 M 的数组保存 N 个键值对,其中 M > N。我们需要依靠数组中的空位解决碰撞冲突。基于这种策略的所有方法被统称为“开放地址”哈希表。线性探测法,就是比较常用的一种“开放地址”哈希表的一种实现方式。线性探测法的核心思想是当冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。简单来说就是:一旦发生冲突,就去寻找下 一个空的散列表地址,只要散列表足够大,空的散列地址总能找到。

    线性探测法的数学描述是:h(k, i) = (h(k, 0) + i) mod m,i表示当前进行的是第几轮探查。i=1时,即是探查h(k, 0)的下一个;i=2,即是再下一个。这个方法是简单地向下探查。mod m表示:到达了表的底下之后,回到顶端从头开始。

    对于开放寻址冲突解决方法,除了线性探测方法之外,还有另外两种比较经典的探测方法,二次探测(Quadratic probing)和双重散列(Double hashing)。但是不管采用哪种探测方法,当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表的操作效率,一般情况下,我们会尽可能保证散列表中有一定比例的空闲槽位。我们用装载因子(load factor)来表示空位的多少。

    散列表的装载因子=填入表中的元素个数/散列表的长度。装载因子越大,说明冲突越多,性能越差。

     

    3.什么是map?

    Map是一个集合,一种依照键(key)存储元素的容器,键(key)很像下标,在List中下标是整数。

    在Map中键(key)可以使任意类型的对象。Map中不能有重复的键(Key),每个键(key)都有一个对应的值(value)。

    一个键(key)和它对应的值构成map集合中的一个元素。

    Map中的元素是两个对象,一个对象作为键,一个对象作为值。键不可以重复,但是值可以重复。

    Map本身是一个接口,要使用Map需要通过子类进行对象实例化。

    Map接口的常用子类有:HashMap、HashTable、TreeMap、ConcurrentHashMap。

    (1)key值不允许重复,如果重复,则会把对应value值更新;

    (2)key和value都允许为null,key为null有且只有一个。

     

    4.什么是HashMap

    1.HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。

    这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。

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

     

    5.HashMap的实现原理或者工作原理?

    HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。

    (1)当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。

    当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。

    (2)当两个对象的hashcode相同会发生什么?

    答:因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。”

    解析:虽然有很多种处理碰撞的方法,这种方法是最简单的,也正是HashMap的处理方法。

    (3)如果两个键的hashcode相同,你如何获取值对象?

    答:当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

    解析:HashMap在链表中存储的是键值对

    (4)如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

    答:将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。

    解析:这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置

    (5)你了解重新调整HashMap大小存在什么问题吗?

    答:当重新调整HashMap大小的时候,会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。

    解析:跳出问题来看,为什么要在多线程的环境下使用HashMap呢?(多线程下不建议用HashMap)

    一句话总结就是,并发环境下HashMap的rehash过程可能会带来循环链表,导致死循环致使线程挂掉。

    因此并发环境下,建议使用Java.util.concurrent包中的ConcurrentHashMap以保证线程安全。

    至于HashTable,它并未使用分段锁,而是锁住整个数组,高并发环境下效率非常的低,会导致大量线程等待。

    同样的,Synchronized关键字、Lock性能都不如分段锁实现的

     

    5.HashMap安全吗?

    不安全,在多线程环境下,1.7 会产生死循环、数据丢失、数据覆盖的问题,1.8 中会有数据覆盖的问题,以1.8为例,

    当A线程判断index位置为空后正好挂起,B线程开始往index位置的写入节点数据,这时A线程恢复现场,执行赋值操作,就把A线程的数据给覆盖了;

    还有++size这个地方也会造成多线程同时扩容等问题。

     

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

            boolean evict) {

            Node<K,V>[] tab; Node<K,V> p; int n, i;

            if ((tab = table) == null || (n = tab.length) == 0)

            n = (tab = resize()).length;

            if ((p = tab[i = (n - 1) & hash]) == null)  //多线程执行到这里

            tab[i] = newNode(hash, key, value, null);

            else {

            Node<K,V> e; K k;

            if (p.hash == hash &&

            ((k = p.key) == key || (key != null && key.equals(k))))

            e = p;

            else if (p instanceof TreeNode) // 这里很重要

            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

            else {

            for (int binCount = 0; ; ++binCount) {

            if ((e = p.next) == null) {

            p.next = newNode(hash, key, value, null);

            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

            treeifyBin(tab, hash);

            break;

            }

            if (e.hash == hash &&

            ((k = e.key) == key || (key != null && key.equals(k))))

            break;

            p = e;

            }

            }

            if (e != null) { // existing mapping for key

            V oldValue = e.value;

            if (!onlyIfAbsent || oldValue == null)

            e.value = value;

            afterNodeAccess(e);

            return oldValue;

            }

            }

            ++modCount;

            if (++size > threshold) // 多个线程走到这,可能重复resize()

            resize();

            afterNodeInsertion(evict);

            return null;

            }

     

  • 相关阅读:
    IE6中overflow:hidden失效怎么办
    单例模式笔记
    linux 中的 "2>&1"含义
    linux 文件目录介绍
    centos 安装jdk
    SimpleDateFormat非线程安全
    Linux下Weblogic 11g R1安装和配置
    <meta>标签 的一些用法
    基于java的邮件群发软件
    史上最完整的集合类总结及hashMap遍历
  • 原文地址:https://www.cnblogs.com/cdlyy/p/12584510.html
Copyright © 2011-2022 走看看