我的面试八股(Java集合篇)

Java集合

两个抽象接口派生:一个是Collection接口,存放单一元素;一个是Map接口存放键值对。

Vector为什么是线程安全

简单,因为官方在可能涉及到线程不安全的操作都进行了synchronized操作,就自身源码就给你加了把锁。

Vector的扩容机制

Vector每次扩容两倍大小(capacityIncrement为0时)
相较于ArrayList,Vecotr多了一个构造方法 :

public Vector(int initialCapacity,    //向量的初始容量
              int capacityIncrement)   //向量溢出时容量增加的量

在这里插入图片描述

ArrayList 的空间浪费

ArrayList的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间。
通过ArrayList扩容源码得知,当列表长度超过数组长度,开始调用 grow() 方法扩容,如果成功扩容1.5倍,就会在列表结尾处留出一定空间。

  /**
     * ArrayList扩容的核心方法。
     */
    private void grow(int minCapacity) {
        // oldCapacity为旧容量,newCapacity为新容量
        int oldCapacity = elementData.length;
        //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
        //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //再检查新容量是否超出了ArrayList所定义的最大容量,
        //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
        //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

RandomAccess接口

RandomAccess接口代码:

public interface RandomAccess {
}

RandomAccess 接口不过是一个标识罢了,ArrayList实现RandomAccess接口表明它有快速访问功能,只是个标识,并不是说它实现RandomAccess接口才具有随机访问功能的。

ArrayList要添加大量元素应该怎么做 / 怎么扩容

在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。

ArrayList核心代码解读(部分)

以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。

/**
     *默认无参构造函数
     *DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

补:JDK6 new 无参构造的 ArrayList 对象时,直接创建了长度是 10 的 Object[] 数组 elementData。

ArrayList扩容机制

  • 当 add 第 1 个元素时,oldCapacity 为 0,经比较后第一个 if 判断成立,newCapacity = minCapacity(为 10)。但是第二个 if 判断不会成立,即 newCapacity 不比 MAX_ARRAY_SIZE 大,则不会进入 hugeCapacity 方法。数组容量为 10,add 方法中 return true,size 增为 1。
  • 当 add 第 11 个元素进入 grow 方法时,newCapacity 为 15,比 minCapacity(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeCapacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。
  • 以此类推······

System.arraycopy() 和 Arrays.copyOf()方法

arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组。

元素排序Comparable和Comparator有什么区别?

先从二者的字面含义来理解它,Comparable 翻译为中文是“比较”的意思,而 Comparator 是“比较器”的意思。Comparable 是以 -able 结尾的,表示它自身具备着某种能力,而 Comparator 是以 -or 结尾,表示自身是比较的参与者,这是从字面含义先来理解二者的不同。

Comparable

Comparable 接口只有一个方法 compareTo,实现 Comparable 接口并重写 compareTo 方法就可以实现某个类的排序了,它支持 Collections.sort 和 Arrays.sort 的排序。

Comparator

Comparator 和 Comparable 的排序方法是不同的,Comparable 排序的方法是 compareTo,而 Comparator 排序的方法是 compare

Comparator 除了可以通过创建自定义比较器外,还可以通过匿名类的方式,更快速、便捷的完成自定义比较器的功能(平常俺习惯在leetcode上刷题其实就是用了comparator)。

Comparable和Comparator使用场景的区别

使用 Comparable 必须要修改原有的类,也就是你要排序那个类,就要在那个中实现 Comparable 接口并重写 compareTo 方法,所以 Comparable 更像是“对内”进行排序的接口。​

而 Comparator 的使用则不相同,Comparator 无需修改原有类。通过 Comparator 接口可以实现和原有类的解耦,在不修改原有类的情况下实现排序功能,所以 Comparator 可以看作是“对外”提供排序的接口。

区别总结

Comparable 和 Comparator 都是用来实现元素排序的,它们二者的区别如下:

  • Comparable 是“比较”的意思,而 Comparator 是“比较器”的意思。
  • Comparable 是通过重写 compareTo 方法实现排序的,而 Comparator 是通过重写 compare 方法实现排序的。
  • Comparable 必须由自定义类内部实现排序方法,而 Comparator 是外部定义并实现排序的。

无序性和不可重复性

  • 无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
  • 不可重复性是指添加的元素按照 equals() 判断时 ,返回 false,需要同时重写 equals() 方法和 hashCode() 方法。

比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同

  • HashSet、LinkedHashSet 和 TreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。
  • HashSet、LinkedHashSet 和 TreeSet 的主要区别在于底层数据结构不同。HashSet 的底层数据结构是哈希表(基于 HashMap 实现)。LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。
  • 底层数据结构不同又导致这三者的应用场景不同。HashSet 用于不需要保证元素插入和取出顺序的场景,LinkedHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet 用于支持对元素自定义排序规则的场景。

Queue的add方法和offer方法的区别

根据因为容量问题而导致操作失败后处理方式的不同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。

抛出异常返回特殊值
add(E e)offer(E e)
remove()poll()
element()peek()

PriorityQueue

PriorityQueue并没有直接实现 Queue接口,而是通过继承 AbstractQueue 类来实现 Queue 接口的一些方法,在 Java 定义中,PriorityQueue 是一个基于优先级的无界优先队列。

与 Queue 的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。这里列举其相关的一些要点:

  • PriorityQueue 利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据。
  • PriorityQueue 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。
  • PriorityQueue 是非线程安全的,且不支持存储 NULL 和 non-comparable 的对象。
  • PriorityQueue 默认是小顶堆,但可以接收一个 Comparator 作为构造参数,从而来自定义元素优先级的先后。

扩容机制

在这里插入图片描述

关于HashMap和HashTable初始容量大小和扩充容量大小的区别

  • 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍
  • 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小。

下面这个方法保证了 HashMap 总是使用 2 的幂作为哈希表的大小。

  /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

HashMap 和 TreeMap 区别

TreeMap 和HashMap 都继承自AbstractMap ,但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap 接口。

实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。
实现SortedMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。

相比于HashMap来说 TreeMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。

HashSet 如何检查重复?

当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。

注:在 JDK1.8 中,实际上无论HashSet中是否已经存在了某元素,HashSet都会直接插入,只是会在add()方法的返回值处告诉我们插入前是否存在相同元素。

HashMap底层实现

JDK1.8之前

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。

JDK1.8之后

JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

注:链表的长度大于 8 的时候,就执行 treeifyBin (转换红黑树)的逻辑。注意这里是执行逻辑,并不是直接转换为红黑树!!(其实就算调用treeifyBin函数进行判断)。treeifyBin 方法中判断是否真的转换为红黑树,判断当前数组的长度是否小于 64,如果当前数组的长度小于 64,那么会选择先进行数组扩容,则才将列表转换为红黑树。

HashMap 的长度为什么是 2 的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。

下标计算方法

为了增强散列性,使元素尽可能的分散开来,从而减少碰撞去使用链表/红黑树,HashMap的发明者采用了位运算的方式来实现一个均匀分布且高效率的函数来求下标。

index = HashCode(Key) & (Length - 1)

只有当HashMap的长度为2的幂时,length-1 正好相当于一个低位掩码,所有二进制位都为1,才能将哈希值的高位全部归零,只保留低位值用来做数组下标访问,且保证范围在length内。
比如下面:
在这里插入图片描述

哈希算法以及扰动函数

HashMap 通过 key 的 hashcode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法,换句话说使用扰动函数之后可以减少碰撞。

前面我们知道了下标的运算方式,是通过低位掩码与哈希值相与得到哈希值的低位来做下标,这就意味着这个哈希值的计算方式几乎决定了HashMap的效率(散列值、碰撞率、分散情况),所以哈希算法的实现很关键。扰动函数就是为此而引入。
(jdk1.7中首次引入扰动函数,但是共做了四次扰动,1.8做了优化只用了一次)

JDK1.7 HashMap的hash方法源码

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    // 这里是扰动函数
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

JDK1.8 HashMap的hash方法源码

    static final int hash(Object key) {
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^ :按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

可以看出扰动函数就是将key的哈希值h与右移16位后的h进行异或运算。

用一句话来概括扰动函数的作用就是:将h的hashCode右移16位并与自身相异或 相当于 使自己的高16位和低16位 相异或得到的值既包含了自己高位的特性又包含了自己低位的特性,从而增加了之后得到的下标的不确定性降低了碰撞的概率
具体来说,如下:
在这里插入图片描述

put函数

  • 通过扰动函数计算key的哈希值
  • 如果哈希表为空,初始化
  • 如果哈希表不为空,计算下标
    1. 如果table[i]为空,创建新节点存入

    2. 如果table[i]不为空,根据HashCode和Key值在链表/红黑树中寻找目标位置并更新其value值
      1. 如果没有发生碰撞
      - JDK1.7将新节点插在当前链表头部的前面然后再下移(头插法
      - JDK1.8

      • 如果是红黑树,调整红黑树的插入
      • 如果是链表,遍历到链表尾并插入新节点(尾插法),遍历的同时计数,当插入新节点后的计数达到阈值,就把链表转化为红黑树

      2.如果发生碰撞,通过比较key的地址或者key的值(equals)遍历链表/红黑树获取旧值,覆盖后返回旧值
      3.如果HashMap容量达到阈值initialCapacity*loadFactor,则进行扩容

我个人对于JDK1.8尾插法的理解:

JDK1.8 新增链表转红黑树的阈值,因此在插入的时候必须知道链表的长度,如果长度超出这个阈值就将其转化为红黑树,因此在插入式必须遍历链表得到链表长度,于是在jdk1.8里插入结点时选择直接插在链表尾部,反正都要遍历一次,这样还保证了在扩容的时候对元素进行transfer时链表的顺序不会像1.7一样倒转,也就不会出现死循环链表。

resize函数

  • JDK1.7: (size >= threshold) && (null != table[bucketIndex])
    存放的键值对数超出阈值,并且新增结点要插入的地方不为空

  • JDK1.8:++size > threshold
    只要存放键值对数超出阈值就扩容

扩容方式

默认扩容16,原数组长度,构建一个原数组长度两倍的新数组,并调用transfer方法将原数组的数组通过重新计算哈希值得到下标再转移到新增数组。

JDK1.7调用indexFor方法重新计算下标,并采用跟插入结点时一致的方式(头插法)挨个移动结点。transfer方法的作用是把原table的Node放到新的table中,使用的是头插法,也就是说,新table中链表的顺序和旧列表中是相反的,在HashMap线程不安全的情况下,这种头插法可能会导致环状节点。比如:线程1准备处理节点,线程2把HashMap扩容成功,链表已经逆向排序,那么线程1在处理节点时就可能出现环形链表。

JDK1.8则是根据规律将原链表拆分为两组,分别记录两个头结点,移动时直接移动头结点。这规律是: HashMap扩容后,原来的元素要么在原位置,要么在原位置+原数组长度 那个位置上

举个例子:

原来的HashMap长度为4,table[2]上存放了A。现在要进行扩容,先创建了一个长度为8的新数组,现在要进行transfer,那么这个A要放到哪里呢?
我们先来根据他原本所在的位置2来倒推,我们知道index = HashCode(Key) & (Length - 1) ,那么就有:
在这里插入图片描述
Hash(key) 可能为010,可能为110。

我们用新的长度(8 = (111)2)和这两个数分别再去通过Hash算法来计算新的下标会发现

  • 010 & 111 = 010 在原位置
  • 110 & 111 = 110 在原位置+4,当前下标+旧数组长度

在转移链表时,结点的转移和插入是一致的,jdk1.7将采用头插法(转移完后链表反转),jdk1.8在分解完链表后直接移动头结点

为什么HashMap是线程不安全的?

主要体现在:

  • JDK1.7中,当多线程操作同一map时,在扩容的时候会因链表反转发生循环链表或丢失数据的情况。
  • JDK1.8中,当多线程操作同一map时,会发生数据覆盖的情况:
    在put的时候,由于put的动作不是原子性的(没有使用Synchronized),线程A在计算好链表位置后,挂起,线程B正常执行put操作,之后线程A恢复,会直接替换掉线程B所put的值,所以依然不是线程安全的。

为什么会遇到ConcurrentModificationException异常?

首先要知道,HashMap中有个属性modCount,用于记录当前map的修改次数,在对map进行put、remove、clear等操作时都会增加modCount。

他的作用体现在对map进行遍历的时候,我们知道HashMap不是线程安全的,当对其进行遍历的时候,会先把modCount赋给迭代器内部的expectedModCount属性。当我们对map进行迭代时,他会时时刻刻比较expectedModCount和modCount是否相等,如果不相等,则说明有其他的线程对同一map进行了修改操作,于是迭代器抛出ConcurrentModificationException异常,告诉程序员有人修改过了。。
这也就是Fail-Fast机制。

什么是Fail-Fast机制?

在系统设计中,快速失效系统一种可以立即报告任何可能表明故障的情况的系统。快速失效系统通常设计用于停止正常操作,而不是试图继续可能存在缺陷的过程。这种设计通常会在操作中的多个点检查系统的状态,因此可以及早检测到任何故障。快速失败模块的职责是检测错误,然后让系统的下一个最高级别处理错误。
例子见上一条。

场景:java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

什么是Fail-Safe机制

使用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,再拷贝的集合上进行遍历。由于迭代时候是对原集合的拷贝进行遍历,所以在遍历的过程中对原集合所作的修改不能被迭代器所检测到,不会触发Concurrent Modification Exception。

缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

如何避免快速失败?

  • 在单线程的遍历过程中,如果要进行 remove 操作,可以调用迭代器 ListIterator 的 remove 方法而不是集合类的 remove方法。看看 ArrayList 中迭代器的 remove 方法的源码,该方法不能指定元素删除,只能 remove 当前遍历元素。
  • 使用并发包(java.util.concurrent)中的类来代替 ArrayList 和 hashMap
    1. CopyOnWriterArrayList代替ArrayList
    2. ConcurrentHashMap代替HashMap

HashMap为什么要引入红黑树?

将查找的时间复杂度从o(n)提升到o(logn)。在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n),加快检索速率

HashMap为什么树化标准是8个?

概率统计。源码注释里也写了,如果 hashCode的分布离散良好的话,那么红黑树是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,注释中给我们展示了1-8长度的具体命中概率,当长度为8的时候,概率概率仅为0.00000006,趋近于零,因此这是根据概率统计决定的。

HashMap为什么退化为链表的阈值是6?

下面纯属个人理解。。。
当链表长度达到阈值8的时候会转为红黑树,但是红黑树退化为链表的阈值却是6,为什么不是小于8就退化呢?比如说7的时候就退化,偏偏要小于或等于6?
主要是一个过渡,避免链表和红黑树之间频繁的转换。如果阈值是7的话,删除一个元素红黑树就必须退化为链表,增加一个元素就必须树化,来回不断的转换结构无疑会降低性能,所以阈值才不设置的那么临界。
其实感觉设计成5也可以,感觉更多就算这样设计的。。。刁难人

HashMap 有哪几种常见的遍历方式?

HashMap 遍历从大的方向来说,可分为以下 4 类:

  1. 迭代器方式遍历
  2. For Each方式遍历
  3. Lambda表达式遍历(JDK1.8+)
  4. Streams API遍历(JDK1.8+)

每种类型又有不同实现方式,具体的遍历方式可分为7种:

  1. 使用迭代器(Iterator)EntrySet的方式进行遍历
  2. 使用迭代器(Iterator)KeySet的方式进行遍历
  3. 使用For Each EntrySet方式遍历
  4. 使用For Each KeySet方式遍历
  5. 使用Lambda方式遍历
  6. 使用Streams API单线程方式遍历
  7. 使用Streams API多线程的方式遍历

性能分析:EntrySet 之所以比 KeySet 的性能高是因为,KeySet 在循环时使用了 map.get(key),而 map.get(key) 相当于又遍历了一遍 Map 集合去查询 key 所对应的值。注意在使用迭代器或者 for 循环时,其实已经遍历了一遍 Map 集合了,因此再使用 map.get(key) 查询时,相当于遍历了两遍。

而 EntrySet 只遍历了一遍 Map 集合,之后通过代码“Entry<Integer, String> entry = iterator.next()”把对象的 key 和 value 值都放入到了 Entry 对象中,因此再获取 key 和 value 值时就无需再遍历 Map 集合,只需要从 Entry 对象中取值就可以了。

所以,EntrySet 的性能比 KeySet 的性能高出了一倍,因为 KeySet 相当于循环了两遍 Map 集合,而 EntrySet 只循环了一遍。

安全性:我们不能在遍历中使用集合 map.remove() 来删除数据,这是非安全的操作方式,但我们可以使用迭代器的 iterator.remove() 的方法来删除数据,这是安全的删除集合的方式。同样的我们也可以使用 Lambda 中的 removeIf 来提前删除数据,或者是使用 Stream 中的 filter 过滤掉要删除的数据进行循环,这样都是安全的,当然我们也可以在 for 循环前删除数据在遍历也是线程安全的。

总结: hashMap作为一个key-value存储的数据结构,我们知道可以使用key去获取到value,所以map提供了两种方式:1. 获取key,map.keySet();可以获取到对应的key的集合,我们遍历key就获取value;2. 获取entrySet,entry是包含key、value,这样也就获取到了对应的key与value。对于这两种我们可以用迭代器进行遍历或者用For Each进行遍历。在JDK1.8+,我们还可以用Lambda语句遍历和Streams API进行遍历。

综合性能和安全性来看,我们应该尽量使用迭代器(Iterator)来遍历 EntrySet 的遍历方式来操作 Map 集合,这样就会既安全又高效了。

都是线程安全的,ConcurrentHashMap和Hashtable有什么区别?

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

  • 底层数据结构:JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
  • 实现线程安全的方式
    • JDK1.7时候,ConcurrentHashMap 对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
    • JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。
    • Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

为什么ConcurrentHashMap是线程安全的?

JDK1.7底层实现

ConcurrentHashMap 在不同的 JDK 版本中实现是不同的,在 JDK 1.7 中它使用的是数组加链表的形式实现的,而数组又分为:大数组 Segment 和小数组 HashEntry。大数组 Segment 可以理解为 MySQL 中的数据库,而每个数据库(Segment)中又有很多张表 HashEntry,每个 HashEntry 中又有多条数据,这些数据是用链表连接的,如下图所示:
在这里插入图片描述
Segment 本身是基于 ReentrantLock 实现的加锁和释放锁的操作,这样就能保证多个线程同时访问 ConcurrentHashMap 时,同一时间只有一个线程能操作相应的节点,这样就保证了 ConcurrentHashMap 的线程安全了。
也就是说 ConcurrentHashMap 的线程安全是建立在 Segment 加锁的基础上的,所以我们把它称之为分段锁或片段锁,如下图所示:
在这里插入图片描述

JDK1.8底层实现

在 JDK 1.7 中,ConcurrentHashMap 虽然是线程安全的,但因为它的底层实现是数组 + 链表的形式,所以在数据比较多的情况下访问是很慢的,因为要遍历整个链表,而 JDK 1.8 则使用了数组 + 链表/红黑树的方式优化了 ConcurrentHashMap 的实现,具体实现结构如下:

在这里插入图片描述
在 JDK 1.8 中 ConcurrentHashMap 使用的是 CAS + volatile 或 synchronized 的方式来保证线程安全的。

在 JDK 1.8 中,添加元素时首先会判断容器是否为空,如果为空则使用 volatile 加 CAS 来初始化。如果容器不为空则根据存储的元素计算该位置是否为空,如果为空则利用 CAS 设置该节点;如果不为空则使用 synchronize 加锁,遍历桶中的数据,替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了。

我们把上述流程简化一下,我们可以简单的认为在 JDK 1.8 中,ConcurrentHashMap 是在头节点加锁来保证线程安全的,锁的粒度相比 Segment 来说更小了,发生冲突和加锁的频率降低了,并发操作的性能就提高了。而且 JDK 1.8 使用的是红黑树优化了之前的固定链表,那么当数据量比较大的时候,查询性能也得到了很大的提升,从之前的 O(n) 优化到了 O(logn) 的时间复杂度,具体加锁示意图如下:
在这里插入图片描述

关于TreeBin

TreeNode是存储红黑树节点,被TreeBin包装。TreeBin通过root属性维护红黑树的根结点,因为红黑树在旋转的时候,根结点可能会被它原来的子节点替换掉,在这个时间点,如果有其他线程要写这棵红黑树就会发生线程不安全问题,所以在 ConcurrentHashMap 中TreeBin通过waiter属性维护当前使用这棵红黑树的线程,来防止其他线程的进入。

static final class TreeBin<K,V> extends Node<K,V> {
        TreeNode<K,V> root;
        volatile TreeNode<K,V> first;
        volatile Thread waiter;
        volatile int lockState;
        // values for lockState
        //信号量
        static final int WRITER = 1; // set while holding write lock
        static final int WAITER = 2; // set when waiting for write lock
        static final int READER = 4; // increment value for setting read lock
...
}

JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?

  • 线程安全实现方式 :JDK 1.7 采用 Segment 分段锁来保证安全, Segment 是继承自 ReentrantLock。JDK1.8 放弃了 Segment 分段锁的设计,采用 Node + CAS + synchronized 保证线程安全,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点。
  • Hash 碰撞解决方法 : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
  • 并发度 :JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/10234.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

走进Vue【三】vue-router详解

目录&#x1f31f;前言&#x1f31f;路由&#x1f31f;什么是前端路由&#xff1f;&#x1f31f;前端路由优点缺点&#x1f31f;vue-router&#x1f31f;安装&#x1f31f;路由初体验1.路由组件router-linkrouter-view2.步骤1. 定义路由组件2. 定义路由3. 创建 router 实例4. 挂…

【Spark】RDD缓存机制

1. RDD缓存机制是什么&#xff1f; 把RDD的数据缓存起来&#xff0c;其他job可以从缓存中获取RDD数据而无需重复加工。 2. 如何对RDD进行缓存&#xff1f; 有两种方式&#xff0c;分别调用RDD的两个方法&#xff1a;persist 或 cache。 注意&#xff1a;调用这两个方法后并不…

处理用户输入

shell脚本编程系列 传递参数 向shell脚本传递数据的最简单方法是使用命令行参数 比如 ./add 10 30读取参数 bash shell会将所有的命令行参数都指派给位置参数的特殊变量。其中$0对应脚本名、$1是第一个参数、$2是第二个参数&#xff0c;依次类推&#xff0c;直到$9 #!/bin/b…

【星界探索——通信卫星】铱星:从“星光坠落”到“涅槃重生”,万字长文分析铱星卫星系统市场

【星界探索——通信卫星】铱星&#xff1a;从“星光坠落”到“涅槃重生”一、铱星简介二、铱星系统设计思路2.1 工作原理2.2 铱星布局三、铱星优势四、发展历程五、第一代铱星公司的破产原因分析5.1 终端和资费价格高昂&#xff0c;市场用户群体小5.2 财务危机5.3 市场分析不足…

深入讲解Linux内核中常用的数据结构和算法

Linux内核代码中广泛使用了数据结构和算法&#xff0c;其中最常用的两个是链表和红黑树。 链表 Linux内核代码大量使用了链表这种数据结构。链表是在解决数组不能动态扩展这个缺陷而产生的一种数据结构。链表所包含的元素可以动态创建并插入和删除。链表的每个元素都是离散存…

每日一问-ChapGPT-20230409-中医基础-四诊之望诊

文章目录每日一问-ChapGPT系列起因每日一问-ChapGPT-20230409-中医基础-四诊之望诊中医中的望闻问切介绍&#xff0c;以及对应的名家望诊的具体细节望诊拓展当日总结每日一问-ChapGPT系列起因 近来看了新闻&#xff0c;看了各种媒体&#xff0c;抖音&#xff0c;官媒&#xff…

Python 小型项目大全 46~50

# 四十六、百万骰子投掷统计模拟器 原文&#xff1a;http://inventwithpython.com/bigbookpython/project46.html 当你掷出两个六面骰子时&#xff0c;有 17%的机会掷出 7。这比掷出 2 的几率好得多&#xff1a;只有 3%。这是因为只有一种掷骰子的组合给你 2&#xff08;当两个…

ptuning v2 的 chatglm垂直领域训练记录

thunlp chatglm 6B是一款基于海量高质量中英文语料训练的面向文本对话场景的语言模型。 THUDM/ChatGLM-6B: ChatGLM-6B&#xff1a;开源双语对话语言模型 | An Open Bilingual Dialogue Language Model (github.com) 国内的一位大佬把chatglm ptuning 的训练改成了多层多卡并…

期刊论文图片代码复现【由图片还原代码】(OriginMatlab)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密…

【Golang入门】简介与基本语法学习

下面是一篇关于Golang新手入门的博客&#xff0c;记录一下。&#xff08;如果有语言基础基本可以1小时入门&#xff09; 一、什么是Golang&#xff1f; Golang&#xff08;又称Go&#xff09;是一种由谷歌公司开发的编程语言。它是一种静态类型、编译型、并发型语言&#xff0…

【JLink仿真器】盗版检测、连接故障、检测不到芯片问题

【JLink仿真器】盗版检测、连接故障、检测不到芯片问题一、问题描述二、解决方法1、降低驱动&#xff08;解决非法问题以及连接故障&#xff09;2、SWD引脚被锁&#xff08;解决检测不到芯片&#xff09;三、说明一、问题描述 盗版检测&#xff1a;the connected probe appear…

【Linux】网络原理

本篇博客让我们一起来了解一下网络的基本原理 1.网络发展背景 关于网络发展的历史背景这种东西就不多bb了&#xff0c;网上很容易就能找到参考资料&#xff0c;我的专业性欠缺&#xff0c;文章参考意义也不大。这里只做简单说明。 网络发展经过了如下几个模式 独立模式&…

几何算法——4.交线(intersection curve)的表达与参数化、微分性质

几何算法——4.曲面求交的交线&#xff08;intersection curve&#xff09;的表达与参数化、微分性质1 关于曲面求交的交线表达2 交线的微分性质3 交线的参数化4 修正弦长参数化的微分性质1 关于曲面求交的交线表达 两个曲面求交&#xff0c;比较经典的方法是用跟踪法&#xf…

【CSS】绝对定位元素设置 水平 / 垂直 居中 ( 绝对定位元素居中设置 - 先偏移 50% 再回退子元素一半尺寸 | 绝对定位居中设置 )

文章目录一、问题提出二、绝对定位 居中设置1、设置固定尺寸2、先偏移50%再回退固定值三、绝对定位元素 水平 / 垂直 居中一、问题提出 绝对定位 不能通过 设置 margin: auto; 样式的方式 , 设置盒子模型水平居中 ; 相对定位 的 盒子模型 , 并没有脱离标准流限制 , 仍然可以使…

2023-数据质量管理方法总结

一、数据质量保障原则 如何评估数据质量的好坏&#xff0c;业界有不同的标准&#xff0c;阿里主要从4个方面进行评估&#xff1a;完整性、准确性、一致性、及时性&#xff1b; 1.完整性 数据完整性是数据最基础的保障&#xff1b; 完整性&#xff1a;指数据的记录和信息是否…

d2l 文本预处理textDataset

这一节极其重要&#xff0c;重要到本来是d2l的内容我也要归到pyhon封面&#xff0c;这里面class的操作很多&#xff0c;让我娓娓道来&#xff01; 目录 1.要实现的函数 2.读取数据集 3.词元化 4.Vocab类 4.1count_corpus(tokens) 4.2class中的各种self 4.2.1 _token_fr…

KIOPTRIX: LEVEL 4通关详解

环境配置 vulnhub上下载的文件没有vmx 去3的文件里偷一个 记事本打开把所有Kioptrix3_vmware改成Kioptrix4_vmware 然后网卡地址随便改一下 打开后会提示找不到虚拟机,手动选一下就行了 信息收集 漏洞发现 web一上去就是一个登录框 扫路径发现database.sql 但是密码是错的…

Amazon SageMaker简直就是机器学习平台的天花板

一、前言 最近参与了亚马逊云科技【云上探索实验】活动&#xff0c;通过Amazon SageMaker基于Stable Diffusion模型&#xff0c;非常简单快速搭建的第一个AIGC&#xff0c;一开始以为非常复杂&#xff0c;不懂动手操作&#xff0c;但实际上操作非常简单&#xff0c;没有想象中…

【嵌入式Linux】Jetson nano GPIO应用 | 驱动开发 | 官方gpiolib、设备树与chip_driver

GPIO子系统 0.暴露给应用层 应用 $ echo 79 > /sys/class/gpio/export //导出79号gpio 引脚&#xff0c;使得可在应用层访问 $ echo out > /sys/class/gpio/gpio79/direction //设置 为输出 $ echo 1 > /sys/class/gpio/gpio79/value //输出高电平 开灯 $ echo 0…

Spark对正常日志文件清洗并分析

目录 日志文件准备&#xff1a; 一.日志数据清洗&#xff1a; 第一步&#xff1a;数据清洗需求分析&#xff1a; 二.代码实现 2.1 代码和其详解 2.2创建jdbcUtils来连接Mysql数据库 2.3 运行后结果展示&#xff1a; 三、留存用户分析 3.1需求概览 3.2.代码实现 3…