HashMap、ConcurrentHashMap以及HashTable(面试向)

---->HashMap

   在java1.7中,hashmap的数据结构是基于数组+链表的结构,即我们比较熟悉的Entry数组,其包含的(key-value)键值对的形式。在多线程环境下,HashMap进行put操作会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。

   hashmap实现原理参考

   Entry是HashMap中的一个静态内部类。代码如下

 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
        int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        } 

  在java1.8中,hashmap是以 数组+链表+红黑树,由于有红黑树的加入,hashmap性能有了很大程度的优化,但是还是没办法解决在并发环境下的线程安全。

---->HashTable

   hashtabled 和 hashmap 的实现原理几乎一样,差别在于

  • HashMap的键和值都允许有null值存在,而HashTable则不行
  • HashMap是非线程安全的,HashTable是线程安全的
  • 在单线程环境下,HashMap的运行效率是要比HashTable要快得多的(因为HashTable是线程安全,但是其实现的安全的策略牺牲代价太大,get/put所有相关操作都是synchronized的,相当于给整个哈希表加了一个大锁,多线程访问时候,只要有一个线程访问或操作该对象时,则其他线程就只能阻塞,相当于将所有的操作串行化)
  • Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍
  • HashMap的Iterator是fail-fast迭代器。当有其它线程改变了HashMap的结构(增加,删除,修改元素),将会抛出ConcurrentModificationException。不过,通过Iterator的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。JDK8之前的版本中,Hashtable是没有fast-fail机制的。在JDK8及以后的版本中 ,HashTable也是使用fast-fail的。

 ---->ConcurrentHashMap

    在java1.7中,concurrenthashmap的数据结构为 Segment + HashEntryConcurrentHashMap锁分段技术:假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。用一个Segment数组维护所有的键值对,一个Segment对象的数据结构相当于一个HashMap,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。ConcurrentHashMap中的HashEntry相对于HashMap中的Entry有一定的差异性:HashEntry中的value以及next都被volatile修饰,这样在多线程读写过程中能够保持它们的可见性,代码如下:

static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

 

     ConcurrentHashMap不允许Key或者Value的值为NULL

 在java1.8中,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想,接采用transient volatile Node<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率

 并且,ConcurrentHashMap相对于HashTable来说,ConcurrentHashMap的很多操作比如get,clear,iterator 都是弱一致性的,而HashTable是强一致性的。

    何为弱一致性?

    get方法是弱一致的,是什么含义?可能你期望往ConcurrentHashMap底层数据结构中加入一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿。换句话说,put操作将一个元素加入到底层数据结构后,get可能在某段时间内还看不到这个元素,若不考虑内存模型,单从代码逻辑上来看,却是应该可以看得到的。

    因为没有全局的锁,在清除完一个segments之后,正在清理下一个segments的时候,已经清理segments可能又被加入了数据,因此clear返回的时候,ConcurrentHashMap中是可能存在数据的。因此,clear方法是弱一致的。如下:

public void clear() {
    for (int i = 0; i < segments.length; ++i)
        segments[i].clear();
}

     ConcurrentHashMap的迭代器底层原理中,在遍历过程中,如果已经遍历的数组内容发生了变化,迭代器不会抛出ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap迭代器弱一致的表现。

    参考:ConcurrentHashMap能完全替代HashTable吗?

    参考:ConcurrentHashMap总结

 

posted @ 2018-10-04 22:25  凉月缘  阅读(953)  评论(0编辑  收藏  举报
Live2D //博客园自带,可加可不加