面试HashMap之追命5连问

 

 

一、HashMap底层实现数据结构?

总的来讲,HashMap就是数组+链表的组合实现,每一个数组元素存储一个链表的头结点,本质上来讲是哈希表“拉链法”的实现。java

HashMap的链表元素对应的是一个静态内部类Entry,Entry主要包含key,value,next三个元素面试

在Jdk1.8中HashMap的实现方式作了一些改变,可是基本思想仍是没有变得,只是在一些地方作了优化,下面来看一下这些改变的地方,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,在性能上进一步获得提高。算法

二、 如何解决Hash冲突? put方法原理?

HashMap 采用一种所谓的“Hash 算法”来决定每一个元素的存储位置。当程序执行 map.put(String,Obect)方法 时,系统将调用String的 hashCode() 方法获得其 hashCode 值——每一个 Java 对象都有 hashCode() 方法,均可经过该方法得到它的 hashCode 值。获得这个对象的 hashCode 值以后,系统会根据该 hashCode 值来决定该元素的存储位置数组

put方法分析:安全

public V put(K key, V value) {
    //调用putVal()方法完成
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断table是否初始化,不然初始化操做
    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);
                    //链表长度8,将链表转化为红黑树存储
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //key存在,直接覆盖
                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;
}

  

下面将这个过程总结一下:数据结构

一、计算key的hash值,算出元素在底层数组中的下标位置。多线程

二、经过下标位置定位到底层数组里的元素(也有多是链表也有多是树)。并发

三、取到元素,判断放入元素的key是否==或equals当前位置的key,成立则替换value值,返回旧值。app

四、若是是树,循环树中的节点,判断放入元素的key是否==或equals节点的key,成立则替换树里的value,并返回旧值,不成立就添加到树里。函数

五、不然就顺着元素的链表结构循环节点,判断放入元素的key是否==或equals节点的key,成立则替换链表里value,并返回旧值,找不到就添加到链表的最后。

精简一下,判断放入HashMap中的元素要不要替换当前节点的元素,key知足如下两个条件便可替换:

一、hash值相等。

二、==或equals的结果为true。

 

三、 为何String, Interger这样的类适合做为键?

String, Interger这样的类做为HashMap的键是再适合不过了,并且String最为经常使用。 
由于String对象是不可变的,并且已经重写了equals()和hashCode()方法了。


  1.不可变性是必要的,由于为了要计算hashCode(),就要防止键值改变,若是键值在放入时和获取时返回不一样的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其余的优势如线程安全。

2.由于获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是很是重要的。若是两个不相等的对象返回不一样的hashcode的话,那么碰撞的概率就会小些,这样就能提升HashMap的性能。
 

四、HashMap与HashTable的区别?

Hashtable能够看作是线程安全版的HashMap,二者几乎“等价”(固然仍是有不少不一样)。Hashtable几乎在每一个方法上都加上synchronized(同步锁),实现线程安全。

区别
  1.HashMap继承于AbstractMap,而Hashtable继承于Dictionary; 
  2.线程安全不一样。Hashtable的几乎全部函数都是同步的,即它是线程安全的,支持多线程。而HashMap的函数则是非同步的,它不是线程安全的。若要在多线程中使用HashMap,须要咱们额外的进行同步处理; 
  3.null值。HashMap的key、value均可觉得null。Hashtable的key、value都不能够为null; 
  4.迭代器(Iterator)。HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。因此当有其它线程改变了HashMap的结构(增长或者移除元素),将会抛出ConcurrentModificationException。 
  5.容量的初始值和增长方式都不同:HashMap默认的容量大小是16;增长容量时,每次将容量变为“原始容量x2”。Hashtable默认的容量大小是11;增长容量时,每次将容量变为“原始容量x2 + 1”; 
  6.添加key-value时的hash值算法不一样:HashMap添加元素时,是使用自定义的哈希算法。Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。 
  7.速度。因为Hashtable是线程安全的也是synchronized,因此在单线程环境下它比HashMap要慢。若是你不须要同步,只须要单一线程,那么使用HashMap性能要好过Hashtable。

可否让HashMap同步? 
HashMap能够经过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap);

 

五、CurrentHashMap是如何实现并发的?

HashTable容器在竞争激烈的并发环境下表现出效率低下的缘由,是由于全部访问HashTable的线程都必须竞争同一把锁。

那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不一样数据段的数据时,线程间就不会存在锁竞争,从而能够有效的提升并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。

首先将数据分红一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其余段的数据也能被其余线程访问。有些方法须要跨段,好比size()和containsValue(),它们可能须要锁定整个表而而不只仅是某个段,这须要按顺序锁定全部段,操做完毕后,又按顺序释放全部段的锁。

这里“按顺序”是很重要的,不然极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,而且其成员变量实际上也是final的,可是,仅仅是将数组声明为final的并不保证数组成员也是final的,这须要实现上的保证。这能够确保不会出现死锁,由于得到锁的顺序是固定的。

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。

Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap相似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每一个HashEntry是一个链表结构的元素, 每一个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先得到它对应的Segment锁。

JDK1.8中的实现

ConcurrentHashMap取消了segment分段锁,而采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构同样,数组+链表/红黑二叉树。
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提高N倍。

JDK1.8的ConcurrentHashMap的结构图以下:

TreeBin: 红黑二叉树节点Node: 链表节点

相关文章
相关标签/搜索