为啥面试要问hashmap 的原理

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为啥面试要问hashmap 的原理相关的知识,希望对你有一定的参考价值。

HashMap
的工作原理

HashMap
,都知道哪里要用
HashMap
,知道
Hashtable

HashMap
之间的区别
,那么
为何这道面试题如此特殊呢?是因为这道题考察的深度很深。
这题经常出现在高级或中高级
面试中。投资银行更喜欢问这个问题,甚至会要求你实现
HashMap
来考察你的编程能力。
ConcurrentHashMap
和其它同步集合的引入让这道题变得更加复杂。让我们开始探索的
旅程吧!

先来些简单的问题

“你用过
HashMap
吗?”

“什么是
HashMap
?你为什么用到它?”

几乎每个人都会回答“是的”,然后回答
HashMap
的一些特性,譬如
HashMap
可以接

null
键值和值,而
Hashtable
则不能;
HashMap
是非
synchronized;HashMap
很快;
以及
HashMap
储存的是键值对等等。
这显示出你已经用过
HashMap

而且对它相当的熟
悉。但是面试官来个急转直下,从此刻开始问出一些刁钻的问题,关于
HashMap
的更多
基础的细节。面试官可能会问出下面的问题:

“你知道
HashMap
的工作原理吗?”

“你知道
HashMap

get()
方法的工作原理吗?”

你也许会回答“我没有详查标准的
Java API
,你可以看看
Java
源代码或者
Open JDK
。”
“我可以用
Google
找到答案。”

但一些面试者可能可以给出答案,

HashMap
是基于
hashing
的原理,
我们使用
put(key,
value)
存储对象到
HashMap
中,使用
get(key)

HashMap
中获取对象。当我们给
put()
方法传递键和值时,
我们先对键调用
hashCode()
方法,
返回的
hashCode
用于找到
bucket
位置来储存
Entry
对象。”这里关键点在于指出,
HashMap
是在
bucket
中储存键对象和

值对象,作为
Map.Entry
。这一点有助于理解获取对象的逻辑。如果你没有意识到这一点,
或者错误的认为仅仅只在
bucket
中存储值的话,
你将不会回答如何从
HashMap
中获取对
象的逻辑。这个答案相当的正确,也显示出面试者确实知道
hashing
以及
HashMap
的工
作原理。但是这仅仅是故事的开始,当面试官加入一些
Java
程序员每天要碰到的实际场景
的时候,错误的答案频现。下个问题可能是关于
HashMap
中的碰撞探测
(collision
detection)
以及碰撞的解决方法:

“当两个对象的
hashcode
相同会发生什么?”

从这里开始,真正的困惑开始了,一些面
试者会回答因为
hashcode
相同,
所以两个对象是相等的,
HashMap
将会抛出异常,
或者
不会存储它们。然后面试官可能会提醒他们有
equals()

hashCode()
两个方法,并告诉他
们两个对象就算
hashcode
相同,但是它们可能并不相等。一些面试者可能就此放弃,而
另外一些还能继续挺进,他们回答“因为
hashcode
相同,所以它们的
bucket
位置相同,
‘碰撞’会发生。因为
HashMap
使用
LinkedList
存储对象,这个
Entry(
包含有键值对的
Map.Entry
对象
)
会存储在
LinkedList
中。”这个答案非常的合理,虽然有很多种处理碰撞
的方法,这种方法是最简单的,也正是
HashMap
的处理方法。但故事还没有完结,面试
官会继续问:

“如果两个键的
hashcode
相同,
你如何获取值对象?”

面试者会回答:
当我们调用
get()
方法,
HashMap
会使用键对象的
hashcode
找到
bucket
位置,然后获取值对象。面试官
提醒他如果有两个值对象储存在同一个
bucket

他给出答案
:
将会遍历
LinkedList
直到找到
值对象。
面试官会问因为你并没有值对象去比较,
你是如何确定确定找到值对象的?除非面
试者直到
HashMap

LinkedList
中存储的是键值对,否则他们不可能回答出这一题。

其中一些记得这个重要知识点的面试者会说,
找到
bucket
位置之后,
会调用
keys.equals()
方法去找到
LinkedList
中正确的节点,最终找到要找的值对象。完美的答案!
参考技术A 我用笔记本给最佳答案排了一下版,给大家贴出来。虽说排版确实很乱,但是答案不得不给一个大赞。

HashMap的工作原理

HashMap,都知道哪里要用HashMap,知道Hashtable和HashMap之间的区别,那么为何这道面试题如此特殊呢?是因为这道题考察的深度很深。
这题经常出现在高级或中高级面试中。投资银行更喜欢问这个问题,甚至会要求你实现HashMap来考察你的编程能力。
ConcurrentHashMap和其它同步集合的引入让这道题变得更加复杂。让我们开始探索的旅程吧!

先来些简单的问题

“你用过HashMap吗?”

“什么是HashMap?你为什么用到它?”

几乎每个人都会回答“是的”,然后回答HashMap的一些特性,譬如HashMap可以接受null键值和值,而Hashtable则不能;
HashMap是非synchronized;

HashMap很快;
以及HashMap储存的是键值对等等。
这显示出你已经用过HashMap,而且对它相当的熟悉。

但是面试官来个急转直下,从此刻开始问出一些刁钻的问题,关于HashMap的更多基础的细节。

面试官可能会问出下面的问题:

“你知道HashMap的工作原理吗?”

“你知道HashMap的get()方法的工作原理吗?”

你也许会回答“我没有详查标准的Java API,你可以看看Java源代码或者Open JDK。”“我可以用Google找到答案。”

但一些面试者可能可以给出答案,“HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。

当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。”

这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Entry。这一点有助于理解获取对象的逻辑。

如果你没有意识到这一点,或者错误的认为仅仅只在bucket中存储值的话,你将不会回答如何从HashMap中获取对象的逻辑。

这个答案相当的正确,也显示出面试者确实知道hashing以及HashMap的工作原理。

但是这仅仅是故事的开始,当面试官加入一些Java程序员每天要碰到的实际场景的时候,错误的答案频现。

下个问题可能是关于HashMap中的碰撞探测(collision detection)以及碰撞的解决方法:

“当两个对象的hashcode相同会发生什么?”

从这里开始,真正的困惑开始了,一些面试者会回答因为hashcode相同,所以两个对象是相等的,HashMap将会抛出异常,或者不会存储它们。

然后面试官可能会提醒他们有equals()和hashCode()两个方法,并告诉他们两个对象就算hashcode相同,但是它们可能并不相等。

一些面试者可能就此放弃,而另外一些还能继续挺进,他们回答“因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。

因为HashMap使用LinkedList存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在LinkedList中。

这个答案非常的合理,虽然有很多种处理碰撞的方法,这种方法是最简单的,也正是HashMap的处理方法。但故事还没有完结,面试官会继续问:

“如果两个键的hashcode相同,你如何获取值对象?”

面试者会回答:当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。

面试官提醒他如果有两个值对象储存在同一个bucket,他给出答案:将会遍历LinkedList直到找到值对象。面试官会问因为你并没有值对象去比较,你是如何确定确定找到值对象的?

除非面试者直到HashMap在LinkedList中存储的是键值对,否则他们不可能回答出这一题。

其中一些记得这个重要知识点的面试者会说,找到bucket位置之后,会调用keys.equals()方法去找到LinkedList中正确的节点,最终找到要找的值对象。
完美的答案!
参考技术B

“HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。

    从这里开始,真正的困惑开始了,一些面试者会回答因为hashcode相同,所以两个对象是相等的,HashMap将会抛出异常,或者不会存储它们。然后面试官可能会提醒他们有equals()和hashCode两个方法,并告诉他们两个对象就算hashcode相同,但是它们可能并不相等。

    一些面试者可能就此放弃,而另外一些还能继续挺进,他们回答“因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用LinkedList存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在LinkedList中。

    ”这个答案非常的合理,虽然有很多种处理碰撞的方法,这种方法是最简单的,也正是HashMap的处理方法。但“如果两个键的hashcode相同,你如何获取值对象” 。

    面试者会回答:当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。面试官提醒他如果有两个值对象储存在同一个bucket,他给出答案:将会遍历LinkedList直到找到值对象。

参考技术C 这个是基础题呀,主要看你对常用的东西是否真的了解
如果只是会用,而不了解原理,他们就认为你没有专研的精神
也就是说潜力不大

面试官:你说 HashMap 线程不安全,它为啥不安全呢?


扫描下方海报 试读 

面试官:你说 HashMap 线程不安全,它为啥不安全呢?


面试官:你说 HashMap 线程不安全,它为啥不安全呢?

本文来源:

http://cnblogs.com/developer_chan/p/10450908.html


我们都知道HashMap是线程不安全的,在多线程环境中不建议使用,但是其线程不安全主要体现在什么地方呢,本文将对该问题进行解密。

1.jdk1.7中的HashMap

在jdk1.8中对HashMap做了很多优化,这里先分析在jdk1.7中的问题,相信大家都知道在jdk1.7多线程环境下HashMap容易出现死循环,这里我们先用代码来模拟出现死循环的情况:

public class HashMapTest {

    public static void main(String[] args{
        HashMapThread thread0 = new HashMapThread();
        HashMapThread thread1 = new HashMapThread();
        HashMapThread thread2 = new HashMapThread();
        HashMapThread thread3 = new HashMapThread();
        HashMapThread thread4 = new HashMapThread();
        thread0.start();
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

class HashMapThread extends Thread {
    private static AtomicInteger ai = new AtomicInteger();
    private static Map  map =  new HashMap<>();

    @ Override
    public void run() 
{
         while (ai. get() <  1000000) {
            map.put(ai. get(), ai. get());
            ai.incrementAndGet();
        }
    }
}

上述代码比较简单,就是开多个线程不断进行put操作,并且HashMap与AtomicInteger都是全局共享的。

在多运行几次该代码后,出现如下死循环情形:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

其中有几次还会出现数组越界的情况:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

这里我们着重分析为什么会出现死循环的情况,通过jps和jstack命名查看死循环情况,结果如下:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

从堆栈信息中可以看到出现死循环的位置,通过该信息可明确知道死循环发生在HashMap的扩容函数中,根源在transfer函数中,jdk1.7中HashMap的transfer函数如下:

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry  e : table) {
             while( null != e) {
                Entry   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进行扩容到newTable后,需要将原来数据转移到newTable中,注意10-12行代码,这里可以看出在转移元素的过程中,使用的是头插法,也就是链表的顺序会翻转,这里也是形成死循环的关键点。

下面进行详细分析。

1.1 扩容造成死循环分析过程

前提条件,这里假设:

  1. hash算法为简单的用key mod链表的大小。

  2. 最开始hash表size=2,key=3,7,5,则都在table[1]中。

  3. 然后进行resize,使size变成4。

未resize前的数据结构如下:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

如果在单线程环境下,最后的结果如下:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

这里的转移过程,不再进行详述,只要理解transfer函数在做什么,其转移过程以及如何对链表进行反转应该不难。

然后在多线程环境下,假设有两个线程A和B都在进行put操作。线程A在执行到transfer函数中第11行代码处挂起,因为该函数在这里分析的地位非常重要,因此再次贴出来。

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

此时线程A中运行结果如下:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

线程A挂起后,此时线程B正常执行,并完成resize操作,结果如下:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

这里需要特别注意的点:由于线程B已经执行完毕,根据Java内存模型,现在newTable和table中的Entry都是主存中最新值:7.next=3,3.next=null。

此时切换到线程A上,在线程A挂起时内存中值如下:e=3,next=7,newTable[3]=null,代码执行过程如下:

newTable[3]=e ----> newTable[3]=3
e=next ----> e=7

此时结果如下:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

继续循环:

e=7
next=e.next ----> next=3【从主存中取值】
e.next=newTable[3----> e.next=3【从主存中取值】
newTable[3]=e ----> newTable[3]=7
e=next ----> e=3

结果如下:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

再次进行循环:

e=3
next=e.next ----> next=null
e.next=newTable[3] ----> e.next=7 即:3.next=7
newTable[3]=e ----> newTable[3]=3
e=next ----> e=null

注意此次循环:e.next=7,而在上次循环中7.next=3,出现环形链表,并且此时e=null循环结束。

结果如下:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

在后续操作中只要涉及轮询hashmap的数据结构,就会在这里发生死循环,造成悲剧。

1.2 扩容造成数据丢失分析过程

遵照上述分析过程,初始时:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

线程A和线程B进行put操作,同样线程A挂起:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

此时线程A的运行结果如下:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

此时线程B已获得CPU时间片,并完成resize操作:

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

同样注意由于线程B执行完成,newTable和table都为最新值:5.next=null。

此时切换到线程A,在线程A挂起时:e=7,next=5,newTable[3]=null。

执行newtable[i]=e,就将7放在了table[3]的位置,此时next=5。接着进行下一次循环:

e=5
next=e.next ----> next=null,从主存中取值
e.next=newTable[1----> e.next=5,从主存中取值
newTable[1]=e ----> newTable[1]=5
e=next ----> e=null

将5放置在table[1]位置,此时e=null循环结束,3元素丢失,并形成环形链表。并在后续操作hashmap时造成死循环。

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

2.jdk1.8中HashMap

在jdk1.8中对HashMap进行了优化,在发生hash碰撞,不再采用头插法方式,而是直接插入链表尾部,因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全,这里我们看jdk1.8中HashMap的put操作源码:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict
{
        Node [] tab; Node  p;  int n, i;
         if ((tab = table) ==  null || (n = tab.length) ==  0)
            n = (tab = resize()).length;
         if ((p = tab[i = (n -  1) & hash]) ==  null// 如果没有hash碰撞则直接插入元素
            tab[i] = newNode(hash, key,  valuenull);
         else {
            Node  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 )p).putTreeVal( this, tab, hash, key,  value);
             else {
                 for ( int binCount =  0; ; ++binCount) {
                     if ((e = p.next) ==  null) {
                        p.next = newNode(hash, key,  valuenull);
                         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();
        afterNodeInsertion(evict);
         return  null;
    }

这是jdk1.8中HashMap中put操作的主函数, 注意第6行代码,如果没有hash碰撞则会直接插入元素。

如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。

假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。

总结

首先HashMap是线程不安全的,其主要体现:

  1. 在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。

  2. 在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。

END

如有收获,请划至底部,点击“在看”,谢

BAT架构经验倾囊相授

以上是关于为啥面试要问hashmap 的原理的主要内容,如果未能解决你的问题,请参考以下文章

面试高频问题:HashMap实现原理

面试官:你说 HashMap 线程不安全,它为啥不安全呢?

头条面试之----HashMap原理

Android面试之HashMap的实现原理

一文搞定HashMap的实现原理和面试

HashMap原理(转)