深入理解HashMap(底层,put源码分析,HashTable,ConcurrentHashMap,HashSet,LinkedSet,LinkedList

一、Map集合

1、HashMap

1、描述

  • HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现。设计目标是尽量实现哈希表O(1)级别的增删改查效果,与HashTable主要区别为**不支持同步和允许null作为key和value**。
  • HashMap线程不安全,主要表现在:
    • 多线程同时put时可能会丢失值(前面的put被后面的覆盖)。
    • 多线程扩容时会出现环状结构,造成死循环。
    • 多线程使用迭代器时会触发fast-fail机制。

2、底层实现

  • JDK1.8之前:

    1、实现:

    采用 数组+链表 实现,形成一个数组带着多个桶的结构,每个数组元素就是一个桶,而数组索引就是每个桶中链表的表头,每一个Map元素的数据结构是一个 Entry

    • 数组存储区间连续,占用内存较多,寻址容易,插入和删除困难。
    • 链表存储区间离散,占用内存较少,寻址困难,插入和删除容易。

    2、遗留问题:

    ​ 在某些极端情况下,会导致大量元素都存放在同一个桶(数组索引是链表的表头)的链表中,此时的HashMap 就相当于一个单链表,假设链表中的元素个数为n个,则其==操作时间复杂度就变成了O(n)==,完全失去了哈希表的优势。

  • JDK1.8及以后:

    1、实现

    ​ 采用 数组+链表+红黑树 实现。每个Map元素的数据结构是 Node(链表)TreeNode(红黑树)

    2、解决遗留问题:

    ​ jdk1.8时默认还是使用 数组+链表 实现,只是元素的数据结构从 Entry 变为了 Node,当存储在同一个桶中的元素过多时,才会将链表转换为红黑树实现,保证其操作时间复杂度为 O(logn)

    3、链表与红黑树的转换条件

    • 链表->红黑树:

      整个 HashMap 中的元素个数 >= 64 同一个桶下的链表中的元素个数 > 8

      此时Map元素的数据结构从 Node 转为 TreeNode ;

    • 红黑树->链表:

      同一个桶下的链表中的元素个数 < 6

      此时元素的数据结构为 Node

    4、链表转红黑树的阈值为什么是8?

    ​ 因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

    ​ 还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

    5、为什么是红黑树,二叉平衡树不行吗?

    • 因为平衡二叉是高度平衡的树, 当平衡被破坏时,需要要 rebalance(再平衡), 开销会比红黑树大.

    • 原因:

      • 插入引起的不平衡:

        插入一个node引起了树的不平衡,平衡二叉树和红黑树都是最多只需要2次旋转操作,即两者都是O(1);

      • 删除引起的不平衡:

        最坏情况下,平衡二叉树需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级是O(logN)此,而红黑树最多只需3次旋转,只需要O(1)的复杂度。

    5、JDK1.8中HashMap的插入源码分析

 /**
     * 将指定参数key和指定参数value插入map中,如果key已经存在,那就替换key对应的value
     * put(K key, V value)可以分为三个步骤:
     * 1.通过hash(Object key)方法计算key的哈希值。
     * 2.通过putVal(hash(key), key, value, false, true)方法实现功能。
     * 3.返回putVal方法返回的结果。
     *
     * @param key   指定key
     * @param value 指定value
     * @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null
     */
    public V put(K key, V value) {
        // 倒数第二个参数false:表示允许旧值替换
        // 最后一个参数true:表示HashMap不处于创建模式
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Map.put和其他相关方法的实现需要的方法
     * putVal方法可以分为下面的几个步骤:
     * 1.如果哈希表为空,调用resize()创建一个哈希表。
     * 2.如果指定参数hash在表中没有对应的桶,即为没有碰撞,直接将键值对插入到哈希表中即可。
     * 3.如果有碰撞,遍历桶,找到key映射的节点
     * 3.1桶中的第一个节点就匹配了,将桶中的第一个节点记录起来。
     * 3.2如果桶中的第一个节点没有匹配,且桶中结构为红黑树,则调用红黑树对应的方法插入键值对。
     * 3.3如果不是红黑树,那么就肯定是链表。遍历链表,如果找到了key映射的节点,就记录这个节点,退出循环。如果没有找到,在链表尾部插入节点。插入后,如果链的长度大于TREEIFY_THRESHOLD这个临界值,则使用treeifyBin方法把链表转为红黑树。
     * 4.如果找到了key映射的节点,且节点不为null
     * 4.1记录节点的vlaue。
     * 4.2如果参数onlyIfAbsent为false,或者oldValue为null,替换value,否则不替换。
     * 4.3返回记录下来的节点的value。
     * 5.如果没有找到key映射的节点(2、3步中讲了,这种情况会插入到hashMap中),插入节点后size会加1,这时要检查size是否大于临界值threshold,如果大于会使用resize方法进行扩容。
     *
     * @param hash         指定参数key的哈希值
     * @param key          指定参数key
     * @param value        指定参数value
     * @param onlyIfAbsent 如果为true,即使指定参数key在map中已经存在,也不会替换value
     * @param evict        如果为false,数组table在创建模式中
     * @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null。
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, i;
        //如果哈希表为空,调用resize()创建一个哈希表,并用变量n记录哈希表长度
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        /**
         * 如果指定参数hash在表中没有对应的桶,即为没有碰撞
         * Hash函数,(n - 1) & hash 计算key将被放置的槽位
         * (n - 1) & hash 本质上是hash % n,位运算更快
         */
        if ((p = tab[i = (n - 1) & hash]) == null)
            //直接将键值对插入到map中即可
            tab[i] = newNode(hash, key, value, null);
        else {// 桶中已经存在元素
            Node<K, V> e;
            K k;
            // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                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;
                    }
                    // 链表节点的<key, value>与put操作<key, value>相同时,不做重复操作,跳出循环
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 找到或新建一个key和hashCode与插入元素相等的键值对,进行put操作
            if (e != null) { // existing mapping for key
                // 记录e的value
                V oldValue = e.value;
                /**
                 * onlyIfAbsent为false或旧值为null时,允许替换旧值
                 * 否则无需替换
                 */
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 访问后回调
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        // 更新结构化修改信息
        ++modCount;
        // 键值对数目超过阈值时,进行rehash
        if (++size > threshold)
            resize();
        // 插入后回调
        afterNodeInsertion(evict);
        return null;
    }

3、各项默认值

  1. 初始容量(capacity):16(1<<4),即**2^4**。

  2. 最大容量:(1<<30),即**2^30**。

  3. 默认扩容因子(loadFactor):0.75

  4. 自动扩容阈值:当前HashMap**元素个数 > capacity * loadFactor**,会自动扩容,并重新hash。

  5. 扩容机制:扩容至**当前HashMap容量*2.0,且每次扩容后的容量必须为2的n次方**。

  6. 容量必须为2的n次方的原因:

    HashMap对元素进行读、写操作时,需要将Map元素的Key的哈希值对数组长度(HashMap 的容量)进行取模运算,结果作为该元素在数组中的索引(index),在计算机中,取模运算的代价远高于位运算的代价,当数组长度为 2^n 时,可以将Map元素的Key的hashcode对 (2^n)-1 进行 与运算(&),效果与对数组长度进行取模运算相等,所以为了提高 HashMap 的操作效率,规定 HashMap 的容量必须为2的n次方,即2^n。

4、线程安全

  1. 是否线程安全?不安全的场景。

    HashMap线程不安全,主要表现在:

    • 多线程同时put时可能会丢失值(前面的put被后面的覆盖)。
    • 多线程扩容时会出现环状结构,造成死循环。
    • 多线程使用迭代器时会触发fast-fail机制。
  2. 如何解决?

    • 使用 CollectionssynchronizedMap() 对其进行包装,使其线程安全。(锁整个表,效率差)
    • 直接使用 线程安全的ConcurrentHashMap。(分段锁/CAS,效率相对较高)

2、HashTable

1、描述

  • Hashtable 继承于 Dictionary 类(Dictionary类声明了操作键值对的接口方法),实现Map接口(定义键值对接口);
  • 与HashMap一样也是哈希表(散列表),存储元素也是键值对。
  • Hashtable 大部分情况下是线程安全的

2、底层实现

​ 继承于 Dictionary 类,实现Map接口,采用 Entry数组+链表 实现。

3、各项默认值

  1. 初始容量(capacity):11
  2. 最大容量:Integer.MAX_VALUE – 8,即**(2^31-1)-8**,可能会导致内存溢出。
  3. 默认扩容因子(loadFactor):0.75
  4. 自动扩容阈值:当前HashMap**元素个数 > capacity * loadFactor**,会自动扩容,并重新hash。
  5. 扩容机制:扩容至**当前HashMap容量*2.0+1**。

4、线程安全

​ Hashtable大部分方法采用 synchronized 修饰,说明Hashtable大部分情况下是线程安全的,但它采用的是 独占锁 机制,并发时会**锁住整个hash表,导致效率十分低下,且在执行一些复合操作时,也会有线程安全隐患。**

复合操作:

  • ​若不存在则添加。
  • 若存在则删除。
  • 等。

5、HashTable与 HashMap 的区别

HashMapHashTable
线程安全不安全大部分情况线程安全
允许为nullkey和value都允许为nullkey和value都不允许为null
初始容量1611
扩容机制扩容至当前容量的2倍扩容至当前容量的2倍+1

3、ConcurrentHashMap

1、描述

​ ConcurrentHashMap 在 JDK1.5 时被加入,是 HashMap 线程安全的版本,其使用方式与 HashMap 一样。

2、底层实现

​ 其他实现与 HashMap 一样,只是添加了线程安全的保障,这里主要讲线程安全的实现

  • JDK1.7:

    • 线程安全采用**分段锁机制来实现,底层数据结构仍然是数组+链表。与HashMap不同的是,ConcurrentHashMap最外层不是一个大的数组,而是一个Segment的数组**,每个Segment下管理一个与HashMap数据结构差不多的table数组,在并发操作加锁时,锁住的是整个Segment,也就是说Segment的个数就是ConcurrentHashMap的最大并发数。
    • 默认有16个Segment,即最大并发数为16。,这个值可以通过构造函数改变,但一经创建就不可更改。
    • 尽管采用分段锁使锁的粒度小了很多,不再像HashTable那样锁住整个Hash表,但JDK1.7的ConcurrentHashMap的并发量还是受到了Segment个数的影响,为了能提供更大的并发量,JDK1.8的ConcurrentHashMap直接抛弃了分段锁机制,转而采用**CAS算法+synchronized**实现。

    [外链图片转存失败(img-t71o1ESE-1563951531836)(C:/Users/Yang/Desktop/%E9%9D%A2%E8%AF%95%E5%87%86%E5%A4%87/img/ConcurrentHashMap-java7.jpg)]

  • JDK1.8及以后:

    • 为进一步提高并发性,JDK1.8放弃了分段锁机制,将锁的级别控制在了更细粒度的**table元素级别上**,也就是说,只需要锁住一个table元素链表的头节点(head),并不会影响其他的table元素的读写,好处在于锁的粒度更细,影响更小,从而并发效率更好。
    • 使用**CAS算法【Compare And Swap】 + synchronized关键字** 来保证**put操作**的线程安全,步骤如下:
      1. 先判断当前被put进Map的元素的Key在table中所对应的数组元素是否为null,若为null,则**通过CAS操作**将其设置为当前put的元素的value。
      2. 如果Key对应的数组元素(也即链表表头或者红黑树的根元素)不为null,则对该数组元素使用synchronized关键字申请锁,然后再进行操作。

    [外链图片转存失败(img-J9lyk2v2-1563951531837)(C:/Users/Yang/Desktop/%E9%9D%A2%E8%AF%95%E5%87%86%E5%A4%87/img/ConcurrentHashMap-java8.jpg)]

    • 详情请参考:基于CAS算法的ConcurrentHashMap

    • CAS算法原理:

      CAS即CompareAndSwap,中文意思是:比较并替换,是由底层硬件提供的一种同步算法,大致原理如下:

      • CAS需要有3个操作数:
        1. 内存地址V:即将要修改的主存变量所在的地址。
        2. 旧的预期值A:即将要修改的主存变量所在的地址的值。
        3. 计算后得到的新值B,即将要更新的目标值。
      • CAS改值操作步骤:
        1. 读取到主存变量的内存地址(V);
        2. 得到内存地址V的值(A)【旧的预期值】;
        3. 通过计算得到新的值(B);
        4. 在将要更新V的值之前,先将A与V的值进行比较(Compare);
        5. 当且仅当A==V的值时,才会将V的值改为B,否则什么都不做。
      • CAS核心:当且仅当内存地址V的值与预期值A相等时,才将内存地址V的值修改为B,否则就什么都不做。整个CAS(比较并替换)操作是一个原子操作。
    • JDK1.8是基于CAS算法的轮询访问改值方式实现同步(即一直循环CAS算法去尝试改值,直到修改成功为止),虽然对CUP的消耗会大些,但如此实现的**线程同步是非阻塞式**的,并发量将得到很大提高。

3、各项默认值

  • 默认最大并发数:16,即为 Segment 的个数。(jdk1.7)
  • 其他默认值与HashMap一致。

二、List集合

1、ArrayList

1、描述

  • ArrayList是List接口可调整大小的数组实现。实现所有可选的List操作,并允许所有元素,包括null,元素可重复。
  • ArrayList是线程不安全的,体现在并发修改时会触发**快速失败(fail-fast)**机制。

2、底层实现

​ 可调整大小的数组实现。

3、各项默认值

  1. 初始容量(capacity):10
  2. 最大容量:Integer.MAX_VALUE - 8,即**Integer.MAX_VALUE - 8**,可能会导致内存溢出。
  3. 默认扩容因子(loadFactor):1
  4. 自动扩容阈值:当前ArrayList**元素个数 > capacity * loadFactor**,会自动扩容。
  5. 扩容机制:扩容至**当前ArrayList容量*1.5**。

4、线程安全

  1. 是否线程安全?不安全场景?

    ArrayList 是**线程不安全**的,并发修改就会触发 **快速失败(fail-fast)**机制。

  2. 如何解决线程不安全

    • 使用 Collections.synchronizedList() 对其进行包装。
    • 使用 CopyOnWriteArrayList 。

2、Vector

1、描述

  • 与ArrayList一样,底层是数组结构。但因它的所有方法都用**synchronized**修饰,所以Vector是线程安全的。
  • 因为线程安全,所以它的效率比ArraList低。

2、底层实现

​ 数组。

3、各项默认值

  1. 初始容量(capacity):10
  2. 最大容量:Integer.MAX_VALUE - 8,即**Integer.MAX_VALUE - 8**,可能会导致内存溢出。
  3. 默认扩容因子(loadFactor):1
  4. 自动扩容阈值:当前ArrayList**元素个数 > capacity * loadFactor**,会自动扩容。
  5. 扩容机制:扩容至**当前ArrayList容量*2**。

4、线程安全

Vector是线程安全的

3、LinkedList

1、描述

  • LinkedList是List和Deque接口的**双向链表**的实现。实现了所有可选List操作,并允许包括null值。
  • LinkedList是线程不安全的。

2、底层实现

双向链表

3、各项默认值

无,因为是双向链表。

4、线程安全

LinkedList是线程不安全的,需要用 Collections.synchronizedList() 对其进行包装。

4、ArrayList、LinkedList、Vector的区别

ArrayListLinkedListVector
底层实现数组双向链表数组
性能索引查询快,添加、删除慢(主要是扩容)添加、删除快,索引查询慢比ArrayList慢
扩容动态扩容双向链表,不用扩容动态扩容
线程安全不安全不安全安全

三、Set集合

1、HashSet

1、描述

  • HashSet是Set接口的实现,元素无序、不可重复,底层是一个HashMap,用以保存数据。
  • 不能保证元素的排列顺序,顺序有可能发生变化。
  • 线程不安全。
  • 集合元素可以是null,但只存在一个null。

2、底层实现

HashMap。

3、各项默认值

​ 与 HashMap 一致。

4、线程安全

HashSet是线程不安全的,需要用 Collections.synchronizedSet() 对其进行包装。

2、LinkedSet

1、描述

  • LinkedSet是Set接口的实现,继承于HashSet,元素不可重复,底层是一个LinkedMap,用以保存数据。
  • LinkedSet**可保证元素的插入顺序。**

2、底层实现

LinkedMap

3、各项默认值

​ 与 HashMap 一致。

4、线程安全

LinkedSet是线程不安全的,需要用 Collections.synchronizedSet() 对其进行包装。

5、与HashSet的区别

​ LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。即**顺序访问性能好,插入、删除性能差。**

3、TreeSet

1、描述

  • TreeSet是SortedSet接口的唯一实现类,元素不可重复。底层是一个TreeMap,用以保存数据。
  • TreeSet**默认可保证元素的自然顺序(元素需要实现Comparable接口),可指定排序规则(需要重写元素的hashCode()方法和equals()方法)**。

2、底层实现

TreeMap

3、各项默认值

​ 与 HashMap 一致。

4、线程安全

TreeSet是线程不安全的,需要用 Collections.synchronizedSet() 对其进行包装。

4、HashSet、LinkedSet、TreeSet的区别

HashSetLinkedSetTreeSet
顺序无序保证插入顺序默认保证自然顺序
底层实现HashMapLinkedMapTreeMap
线程安全不安全不安全不安全

四、Java8新特性

1、速度更快

更改了底层数据结构,如HashMap、HashSet。

2、代码更少

新增语法:lambda表达式

3、有强大的Stream API

Stream API

4、便于并行

优化 Fork/Join 框架。

5、最大化减少空指针异常

新增 Optional 容器类。

6、总结

eSet的区别

HashSetLinkedSetTreeSet
顺序无序保证插入顺序默认保证自然顺序
底层实现HashMapLinkedMapTreeMap
线程安全不安全不安全不安全

四、Java8新特性

1、速度更快

更改了底层数据结构,如HashMap、HashSet。

2、代码更少

新增语法:lambda表达式

3、有强大的Stream API

Stream API

4、便于并行

优化 Fork/Join 框架。

5、最大化减少空指针异常

新增 Optional 容器类。

6、总结

最主要的核心还是 Lambda 表达式Stream API

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值