让人头大的HashMap、HashTable、ConcurrentHashMap该到此为止了!

目录

Map涉及的集合框架体系图

让人头大的HashMap、HashTable、ConcurrentHashMap该到此为止了!

什么是 Map

Map:双列数据,存储key-value对的数据 —类似于高中的函数:y = f(x)

存储结构的理解:

  • Map中的key:无序的、不可重复的,使用Set存储所的key key所在的类要重写equals()和hashCode() (以HashMap为例)
  • Map中的value:无序的、可重复的,使用Collection存储所有的value —>value所在的类要重写equals()
  • 一个键值对:key-value构成了一个Entry对象。
  • Map中的entry:无序的、不可重复的,使用Set存储所的entry

让人头大的HashMap、HashTable、ConcurrentHashMap该到此为止了!

HashMap

基本特点

  • 允许使用null作为key或value(源码写的,没有为什么,别问。)
  • 线程不安全,运行效率高。(源码没加锁)
  • HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。
  • 实现原理基于哈希表,只是说1.7和1.8 解决冲突的方式不同

HashMap在jdk1.7中实现原理:

HashMap map = new HashMap():

在实例化以后,底层创建了长度是16的一维数组Entry[] table。

put

关于put的执行流程(再次之前可能执行过多次的put)

首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
如果此位置上的数据为空,此时的key1-value1添加成功。 ----情况1
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:

  • 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
    如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法比较:
  • 如果equals()返回false:此时key1-value1添加成功。----情况3
    如果equals()返回true:使用value1替换value2。
 补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。
 在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原的数据复制过来。

HashMap在jdk8中相较于jdk7在底层实现方面的不同:

  1. new HashMap():底层没创建一个长度为16的数组,首次调用put()方法时,底层创建长度为16的数组
  2. jdk 8底层的数组是:Node[],而非Entry[]
  3. jdk7底层结构只:数组+链表。jdk8中底层结构:数组+链表+红黑树。
    3.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
    3.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的数据改为使用红黑树存储。

当满足条件个数大于8以后调用treeifyBin方法尝试转化红黑树。但是数组如果长度小于MIN_TREEIFY_CAPACITY(64)就选择扩容,而不是转化为红黑树,这就是为什么s或尝试转换的原因。。

HashMap底层典型属性的属性的说明:

DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64 同时 当树中的元素小于等于6的时候会退化为链表

面试题总结

参考汇总题目

LinkedHashMap的底层实现原理(了解)

LinkedHashMap底层使用的结构与HashMap相同,因为LinkedHashMap继承于HashMap. HashMap和双向链表合二为一即是LinkedHashMap。
同时hashMap是无序的,也就是说我们遍历map的顺序和最初插入的顺序是不一致的。但是LinkedHashMap保证了有序性,遍历map集合的时候,可以保证按照插入的顺序。

保证在遍历map元素时,可以照添加的顺序实现遍历。 原因:在原的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
对于频繁的遍历操作,此类执行效率高于HashMap。

区别就在于:LinkedHashMap内部提供了Entry,替换HashMap中的Node.
让人头大的HashMap、HashTable、ConcurrentHashMap该到此为止了!
让人头大的HashMap、HashTable、ConcurrentHashMap该到此为止了!

HashTbale

Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。
HashMap不能保证线程安全,但HashTable可以,JDK1.0引入的类,是线程安全的,能用于多线程环境中。
HashMap和HashTable的不同
主要区别就是

  • 作为古老的实现类;线程安全的,效率低;
  • 不能存储null的key和value
  • 数据结构是数组+链表
  • .计算hash值方式不同
    为了得到元素的位置,首先需要根据元素的 KEY计算出一个hash值,然后再用这个hash值来计算得到最终的位置。
    ①:HashMap有个hash方法重新计算了key的hash值,因为hash冲突变高,所以通过一种方法重算hash值的方法:
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

注意这里计算hash值,先调用hashCode方法计算出来一个hash值,再将hash与右移16位后相异或,从而得到新的hash值
**②:Hashtable通过计算key的hashCode()**来得到hash值就为最终hash值。

  • HashMap 哈希扩容必须要求为原容量的2倍,而且一定是2的幂次倍扩容结果,而且每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入;而Hashtable扩容为原容量2倍加1;

深入理解参考

具体的加锁机制

Hashtable是在每个方法上都加上了Synchronized完成同步,效率低下。见图。
让人头大的HashMap、HashTable、ConcurrentHashMap该到此为止了!

其有一个子类Properties:
Hashtable的子类,要求key和value都是String。通常与配置文件的读取。

ConcurrentHashMap

上面说到HashMap是线程不安全的,那么在并发的使用场景下,就要使用ConcurrentHashMap来保证安全,ConcurrentHashMap相比于同样线程安全的HashTable,效率要高很多(ConcurrentHashMap采用的锁有synchronized,CAS,自旋锁,分段锁,volatile等;)
ConcurrentHashMap是HashMap的线程安全版本,内部也是使用(数组 + 链表 + 红黑树1.8)的结构来存储元素。
同样也分1.7和1.8两个版本(这个东西它太值得探究了,写源码的小哥哥一次也不可能研究到位,就得一直改啊改)
参考分析

1.7和1.8共同的特性

  • key和value都不允许为null
  • ConCurrentHashMap支持高并发的访问和更新,它是线程安全的
  • 读操作没有加锁其中vauel和next用了volatile 修饰,保证了在内存中的可见性是安全的,从而使得get方法是不用加锁的,是非阻塞的。
  • 读写分离操作提高效率,多线程对不同的segment/Node 的插入/删除是可以并发并行的,对同一个的segment/Node的写操作互斥。读操作无锁。
  • 键、值迭代器是弱一致迭代器,创建迭代器之后,可以对元素进行更新。
  • JDK1.8底层是散列表+红黑树
  • 在任何一个构造方法中,都没有对存储Map元素Node的table变量进行初始化。而是在第一次put操作的时候在进行初始化。

ConcurrentHashMap在1.7中实现原理

数组+链表+分段锁
让人头大的HashMap、HashTable、ConcurrentHashMap该到此为止了!
底层的数据结构还是数组+链表,整个Hash表,segment(段),HashEntry(节点)。每个segment就相当于一个HashTable(一个哈希桶)。这里的数组是一个Segment 数组、链表的每一个结点是HashEntry ,HashEntry和 HashMap 一样,仍然是数组 + 链表。Hash链中的数据都是从头开始插入的。只有next的引用。

保证线程安全的机制

  • 关于Segment
  • Segment是一种可重入锁继承自ReentrantLock,在ConcurrentHashMap里扮演锁的角色。也就是说ConcurrentHashMap实现同步的方式时基于ReentrantLock锁机制的。
  • 分段锁的原理
    在 HashTable中锁是 HashTable这个对象,而在 ConcurrentHashMap是堆同一个segment进行同步枷锁,(这里可以就是理解对一个桶加锁),这样可以基于不同的segment实现并发写操作。

和HashMap一样,同样存在着hash冲突的问题,链表查询效率低。

HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

ConcurrentHashMap在1.8中实现原理

数组+链表+红黑树, 用CAS无锁算法 + synchronized来实现线程安全
让人头大的HashMap、HashTable、ConcurrentHashMap该到此为止了!

保证安全的机制

结构和HashMap的结构一样,其中抛弃了分段锁,采用了CAS+synchronized来保证并发安全,1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。
对一个结点进加锁
在 ConcurrentHashMap锁则是数组中的第一个元素。这样的话,我们比如我们要多数组中下标为0和1的桶进行操作,两个线程就可以同时对0和1进行操作。
1.7是一段一段的多个结点加锁(一个桶加锁),1.8是一个结点加锁(一个桶的第一个元素加锁)应该是这么理解的。

让人头大的HashMap、HashTable、ConcurrentHashMap该到此为止了!

put

到底如果采用了CAS+synchronized,就得看看它的put方法的源码
参考源码分析

public V put(K key, V value) {
        return putVal(key, value, false);
    }

    final V putVal(K key, V value, boolean onlyIfAbsent) {
    	// key 和 value 都不能为 null
        if (key == null || value == null) throw new NullPointerException();
        // 计算 hash  存放数组哪一个索引
        int hash = spread(key.hashCode());
        int binCount = 0;
        // 这里一直自旋,直到放入元素成功
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 如果数组没有被初始化,则去初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // 如果桶中第一个元素没有元素,则通过CAS的方式进行放入    
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // casTabAt --去调用 U.compareAndSwapObject 自旋
            //为什么不是说如果失败就自旋还有重新再去判断一次
            //因为并发操作的时候,有可能一个线程刚开始判断是第一个位置没有元素
            //但是插入失败 其他线程插入成功更改了就会有元素 所以需要再次判断自旋
            //而不是无条件自旋
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 如果当前桶中的元素正在被迁移,则当前线程也帮助迁移元素
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 以桶中第一个元素为锁对象
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                    	// 如果第一个节点是链表节点,则以链表的方式放入元素
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        // 如果当前元素是树节点,则以树的方式放入元素
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                	// 判断是否需要树化
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 键值对个数+1
        addCount(1L, binCount);
        return null;
    }

TreeMap

TreeMap的使用向TreeMap中添加key-value,要求key必须是由同一个类创建的对象
因为要照key进行排序:自然排序 、定制排序
保证照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序
底层使用红黑树

对比总结

让人头大的HashMap、HashTable、ConcurrentHashMap该到此为止了!

上一篇:查找算法:哈希表


下一篇:168-JUC之ConcurrentHashMap(八)