java容器~面试知识点整理

一、导图

在这里插入图片描述

二、详解

2.1 List

2.1.1 ArrayList

ArrayList的底层数据结构就是一个数组,数组元素的类型为Object类型,对ArrayList的所有操作底层都是基于数组的。
ArrayList的线程安全性:对ArrayList进行添加元素的操作的时候是分两个步骤进行的,这个过程在多线程的环境下是不能保证具有原子性的,因此ArrayList在多线程的环境下是线程不安全的 。

2.1.2 LinkedList

LinkedList 的底层数据结构是双向链表。

2.1.3 数组和链表的区别?ArrayList和linkedList有什么区别?

在内存中,数组占用的是一块连续的内存区,而链表在内存中,是分散的,因为这种物理结构上的差异,导致了他们在访问、增加、删除节点这三种操作上所带来的时间复杂度不同。
对于访问,数组在物理内存上是连续存储的,硬件上支持“随机访问”,就是你访问一个a[3]的元素与访问一个a[10000],使用数组下标访问时,这两个元素的时间消耗是一样的。但是对于链表就不是了,链表也没有下标的概念,只能通过头节点指针,从每一个节点,依次往下找。数组和链表时间复杂度分别是O(1)与O(n),方式一种是“随机访问”,一种是“顺序访问”。
对于增加,因为数组在内存中是连续存储的,要想在某个节点之前增加,且保持增加后数组的线性与完整性,必须要把此节点往后的元素依次后移。而链表只需要改变节点中的“指针”,就可以实现增加。自身在内存中所占据的位置不变,只是这个节点所占据的这块内存中数据(指针)改变了,相对于数组的大动作,链表则要显示温和的多,局部数据改写就可以了。
对于删除,和增加是一样的原理。
除了访问、插入、删除的不同外,还有在操作系统
内存管理
方面也有不同。因为数组与链表的物理存储结构不同,在内存预读方面,内存管理会将连续的存储空间提前读入缓存,所以数组往往会被都读入到缓存中,这样进一步提高了访问的效率,而链表由于在内存中分布是分散的,往往不会都读入到缓存中,本来访问效率就低,又没有读入缓存,效率反而更低了。在实际应用中,因为链表带来的动态扩容的便利性,在做为算法的容器方面,用的更普遍一点。

2.2.4 ArrayList和Vector的区别

这两个类都实现了List接口(List接口继承了Collection接口),他们都是有序集合,即存储在这两个集合中的元素的位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引号取出某个元素,并且其中的数据是允许重复的,这是与HashSet之类的集合的最大不同处,HashSet之类的集合不可以按索引号去检索其中的元素,也不允许有重复的元素。
ArrayList与Vector的区别主要包括两个方面:.
(1)同步性:
Vector是线程安全的 方法上加了synchronized,也就是说是它的方法之间是线程同步的,而ArrayList是线程序不安全的。
(2)数据增长:
ArrayList与Vector都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加ArrayList与Vector的存储空间,每次要增加存储空间时,Vector默认增长为原来两倍,而ArrayList的增长策略在文档中没有明确规定(从源代码看到的是增长为原来的1.5倍)。

2.2 Set

Set是没有重复元素的集合,是无序的。

2.2.1 HashSet

HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变,此类允许使用null元素。
在HashSet中,元素都存到HashMap键值对的Key上面,而Value时有一个统一的值private static final Object PRESENT = new Object();,(定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。)

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

hashSet底层是使用HashMap,通过比较key的hash值来保证元素的不可重复。
hashSet 为什么是无序的?
所谓的有序无序意思就是是否可以按照插入的顺序取出元素,HashSet底层使用的是HashMap,HashMap的底层使用的数组+链表,插入元素的时候通过比较hash值来决定插入的位置,所以说hashSet是无序的。

2.2.2 TreeSet

TreeSet是一个有序的集合类,TreeSet的底层是通过TreeMap实现的。TreeSet并不是根据插入的顺序来排序,而是根据实际的值的大小来排序。TreeSet也支持两种排序方式:

  • 自然排序
  • 自定义排序
2.2.3 LinkedHashSet

LinkedHashSet继承自HashSet,源码更少、更简单,唯一的区别是LinkedHashSet内部使用的是LinkHashMap。这样做的意义或者好处就是LinkedHashSet中的元素顺序是可以保证的,也就是说遍历序和插入序是一致的。

2.3 Map

2.3.1 hashMap

HashMap在多线程情况下,在put的时候,插入的元素超过了容量(由负载因子<0.75>决定)的范围就会触发扩容操作,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。
在这里插入图片描述
这个仅仅是示意图,没有考虑到数组要扩容的情况。

  • 大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表
  • capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
  • loadFactor:负载因子,默认为 0.75。
  • threshold:扩容的阈值,等于 capacity * loadFactor

Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素达到了 8 个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
在这里插入图片描述

Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode
hashMap为什么用数据+链表?
为什么用链表 -->因为要处理hash冲突,即Key不同但是生成的hashcode一样的情况,只有key,hashcode都一样才可以覆盖,不然是要保存起来的,这时候用链表.

2.3.2 hashTable

HashTable,它是线程安全的,它在所有涉及到多线程操作的都加上了synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争一把锁,在多线程的环境下,它是安全的,但是无疑是效率低下的。
HashTable he HashMap 对key和value的空值允许情况:

  • HashTable中key和value都不允许为null;
  • HashMap中空值可以作为Key,也可以有一个/多个Key的值为空值;

HashMap和HashTable内部的数组初始化和扩容方式也不相同

  • HashMap的hash数组默认长度大小为16,扩容方式为2的指数:length_HashMap = 16 * 2n(n为扩容次数)
  • HashTable的hash数组默认长度大小为11,扩容方式为两倍加一:length_HashTable = 上一次HashTable数组长度 * 2 + 1
2.3.3 TreeMap
2.3.4 LinkedHashMap

TreeMap和LinkedHashmap都是有序的。(TreeMap默认是key升序,LinkedHashmap默认是数据插入顺序)

  • TreeMap是基于比较器Comparator来实现有序的。
  • LinkedHashmap是基于链表来实现数据插入有序的。

2.4 Queue

队列是一种特殊的线性表,它只允许在表的前端进行删除操作,而在表的后端进行插入操作。LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用。

  • Queue 实现通常不允许插入 null 元素
  • 队列通常(但并非一定)以 FIFO(先进先出)的方式排序各个元素。
  • 在处理元素前用于保存元素的 collection。除了基本的 Collection 操作外,队列还提供其他的插入、提取和检查操作。每个方法都存在两种形式:一种抛出异常(操作失败时),另一种返回一个特殊值(null 或 false,具体取决于操作)。

2.5 数组

三、高级

3.1 CopyOnWriteArrayList

并发包中的并发list只有CopyOnWriteArrayList。CopyOnWriteArrayList是一个线程安全的list,对其进行的修改操作都是在底层的一个复制的数组上进行的。
CopyOnWriteArrayList 使用写时复制的策略来保证list 的一致性,而获取一修改一写入三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个时间只有一个线程能对list 数组进行修改。另外
CopyOnWriteArrayList 提供了弱一致性的法代器, 从而保证在获取迭代器后,其他线程对list 的修改是不可见的, 迭代器遍历的数组是一个快照。另外, CopyOnWriteArraySet 的底层就是使用它实现的。
详解:https://www.yuque.com/haolonglong/msvmro/fzfgyi#3C2tE

3.2 ConcurrentHashMap

在这里插入图片描述

ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。
整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁
简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
ConcurrentHashMap 默认有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。
再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。
在这里插入图片描述

Java7 中实现的 ConcurrentHashMap 说实话还是比较复杂的,Java8 对 ConcurrentHashMap 进行了比较大的改动。可以参考 Java8 中 HashMap 相对于 Java7 HashMap 的改动,对于 ConcurrentHashMap,Java8 也引入了红黑树。

JAVA8的ConcurrentHashMap为什么放弃了分段锁,有什么问题吗?

官方文档说明:

  1. 加入多个分段锁浪费内存空间。
  2. 生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
  3. 为了提高 GC 的效率

使用CAS操作来确保一些操作的原子性,取代了锁。但是ConcurrentHashMap的一些操作使用了synchronized锁,而不是ReentrantLock。

3.3 并发队列

3.3.1 ConcurrentlinkedQueue

ConcurrentLinkedQueue 是线程安全的无界非阻塞队列,其底层数据结构使用单向链表实现,对于入队和出队操作使用CAS 来实现线程安全。
ConcurrentLinkedQueue 的底层使用单向链表数据结构来保存队列元素,每个元素被包装成一个Node 节点。队列是靠头、尾节点来维护的,创建队列时头、尾节点指向一个item 为null 的哨兵节点。由于使用非阻塞CAS 算法,没有加锁,所以在计算size 时有可能进行了offer 、poll 或者remove 操作, 导致计算的元素个数不精确,所以在井发情况下size 函数不是很有用。
详细地址:https://www.yuque.com/haolonglong/msvmro/ayy4hz#0sOc1

3.3.2 LinkedBlockingQueue

LinkedBlockingQueue 的内部是通过单向链表实现的,使用头、尾节点来进行入队和出队操作,也就是入队操作都是对尾节点进行操作,出队操作都是对头节点进行操作。
如下图所示,对头、尾节点的操作分别使用了单独的独占锁从而保证了原子性,所以出队和入队操作是可以同时进行的。另外对头、尾节点的独占锁都配备了一个条件队列,用来存放被阻塞的线程,并结合入队、出队操作实现了一个生产消费模型。

在这里插入图片描述

详细地址:https://www.yuque.com/haolonglong/msvmro/ng7y0i#nzdjd

3.3.3 ArrayBlockingQueue

有界数组方式实现的阻塞队列ArrayBlockingQueue 。
如下图所示, ArrayBlockingQueue 通过使用全局独占锁实现了同时只能有一个线程进行入队或者出队操作,这个锁的粒度比较大,有点类似于在方法上添加synchronized的意思。
其中 offer 和poll 操作通过简单的加锁进行入队、出队操作,而put 、take 操作则使用条件变量实现了,如果队列满则等待,如果队列空则等待,然后分别在出队和入队操作中发送信号激活等待线程实现同步。
另外,相比LinkedBlockingQueue, ArrayBlockingQueue 的size 操作的结果是精确的, 因为计算前加了全局锁。
在这里插入图片描述

ArrayBlockingQueue在一般的应用场景下已经足够。对于超高并发的环境,由于生产者-消息者共用一把锁,可能出现性能瓶颈。另外,由于ArrayBlockingQueue是有界的,且在初始时指定队列大小,所以如果初始时需要限定消息队列的大小,则ArrayBlockingQueue 比较合适。
详细地址:https://www.yuque.com/haolonglong/msvmro/plfxh9

3.3.4 PriorityBlockingQueue

PriorityBlockingQueue是一种无界阻塞队列,内部是使用平衡二叉树堆实现的,在构造的时候可以指定队列的初始容量。具有如下特点:

  1. PriorityBlockingQueue与之前介绍的阻塞队列最大的不同之处就是:它是一种优先级队列,也就是说元素并不是以FIFO的方式出/入队,而是以按照权重大小的顺序出队;
  2. PriorityBlockingQueue是真正的无界队列(仅受内存大小限制),它不像ArrayBlockingQueue那样构造时必须指定最大容量,也不像LinkedBlockingQueue默认最大容量为Integer.MAX_VALUE
  3. 由于PriorityBlockingQueue是按照元素的权重进入排序,所以队列中的元素必须是可以比较的,也就是说元素必须实现Comparable接口;
  4. 由于PriorityBlockingQueue无界队列,所以插入元素永远不会阻塞线程;
  5. PriorityBlockingQueue底层是一种基于数组实现的堆结构。

详细地址:https://www.yuque.com/haolonglong/msvmro/kbww4y

3.3.5 DelayQueue

DelayQueue 并发队列是一个无界阻塞延迟队列,队列中的每个元素都有个过期时间,当从队列获取元素时,只有过期元素才会出队列。队列头元素是最快要过期的元素。
详细地址:https://www.yuque.com/haolonglong/msvmro/yqyfcd#XhPfq

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值