Java集合
ArrayList 和 Array(数组)的区别?
1、ArrayList不需要指定大小,可以动态的扩容。而Array需要指定大小,不能动态扩容。
2、ArrayList 允许你使用泛型来确保类型安全,Array 则不可以使用泛型。
3、ArrayList 中只能存储对象,Array 可以直接存储基本类型数据,也可以存储对象。
4、ArrayList 支持插入、删除、遍历等常见操作,并且提供了丰富的 API 操作方法。
HashMap为什么要求主数组的长度为2^n呢?
1、 在通过hash值来计算元素位置的时候, 是用hash对数组的长度table.length取余数,但是底层我们采用了一个等效公式是 (hash & table.length - 1),因为与运算效率高。这里等效的要求就是数组长度table.size必须是2的n次倍! 所以HashMap源码中的tableSizeFor保证了哈希表的长度是2的幂次方。
当构造函数指定了初始化容量 initialCapacity ,也只是通过 tableSizeFor 将其扩容到与 initialCapacity 最接近的 2 的幂次方大小,然后暂时赋值给 threshold ,后续通过 resize 方法将 threshold 赋值给 newCap 进行 table 的初始化。
HashMap 多线程操作导致死循环问题
JDK1.7 及之前版本的 HashMap 在多线程环境下扩容操作可能存在死循环问题,如果多个线程同时将元素插入到同一个桶位的链表中,头插法可能会导致链表形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。
为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。
HashMap 为什么线程不安全?
1、同时插入元素时造成死循环
2、多线程造成数据覆盖问题
JDK1.7 及之前版本,在多线程环境下,HashMap 扩容时会造成死循环和HashMap数据丢失的问题。
数据丢失这个在 JDK1.7 和 JDK 1.8 中都存在,这里以 JDK 1.8 为例进行介绍。
JDK 1.8 后,在 HashMap 中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对 HashMap 的 put 操作会导致线程不安全,具体来说会有数据覆盖的风险。
HashMap 常见的遍历方式?
HashMap 遍历从大的方向来说,可分为以下 4 类:
1、迭代器(Iterator)方式遍历;
2、For Each 方式遍历;
3、Lambda 表达式遍历(JDK 1.8+);
4、Streams API 遍历(JDK 1.8+)。
但每种类型下又有不同的实现方式,因此具体的遍历方式又可以分为以下 7 种:
使用迭代器(Iterator)EntrySet 的方式进行遍历;
使用迭代器(Iterator)KeySet 的方式进行遍历;
使用 For Each EntrySet 的方式进行遍历;
使用 For Each KeySet 的方式进行遍历;
使用 Lambda 表达式的方式进行遍历;
使用 Streams API 单线程的方式进行遍历;
使用 Streams API 多线程的方式进行遍历。
说⼀说 ArrayList 的扩容机制吧
1、以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。
int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)!
ConcurrentHashMap 为什么 key 和 value 不能为 null?
因为如果设置为null,就会存在二义性,即不能判断是不存在还是就是为null值,并且因为hashmap正确使用场景是单线程下,由于是单线程,当得到一个key是null的时候,可以用hashMap.containsKey(key)方法来区分上面说的二义性。而如果是ConcurrentHashMap中,是可以在多线程环境下使用的,在一个线程得到的key为null和使用containsKey(key),这之间可能有其他线程执行操作,导致不能区分这种二义性。
ConcurrentHashMap 能保证复合操作的原子性吗?
不能,复合操作是指由多个基本操作(如put、get、remove、containsKey等)组成的操作,例如先判断某个键是否存在containsKey(key),然后根据结果进行插入或更新put(key, value)。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。
LinkedList 为什么不能实现 RandomAccess 接口?
RandomAccess
是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于 LinkedList
底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 RandomAccess
接口。
什么是阻塞队列BlockingQueue?
阻塞队列就说基于非空和非满两个条件实现生产者和消费者之间的交互,BlockingQueue
阻塞的原因是其支持当队列没有元素时一直阻塞,直到有元素;还支持如果队列已满,一直阻塞直到队列可以放入新元素时再放入。最常见的例子无非是我们的线程池,从源码中我们就能看出当核心线程无法及时处理任务时,这些任务都会扔到 workQueue
中。
BlockingQueue` 的实现类:`ArrayBlockingQueue`、`LinkedBlockingQueue`、`PriorityBlockingQueue
ArrayBlockingQueue 是什么?它的特点是什么?
ArrayBlockingQueue
是 BlockingQueue
接口的有界队列实现类,常用于多线程之间的数据共享,底层采用数组实现。
ArrayBlockingQueue
的容量有限,一旦创建,容量不能改变。
为了保证线程安全,ArrayBlockingQueue
的并发控制采用可重入锁 ReentrantLock
,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。并且,它还支持公平和非公平两种方式的锁访问机制,默认是非公平锁。
ArrayBlockingQueue
虽名为阻塞队列,但也支持非阻塞获取和新增元素(例如 poll()
和 offer(E e)
方法),只是队列满时添加元素会抛出异常,队列为空时获取的元素为 null,一般不会使用。
ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
ArrayBlockingQueue
和 LinkedBlockingQueue
是 Java 并发包中常用的两种阻塞队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别:
- 底层实现:
ArrayBlockingQueue
基于数组实现,而LinkedBlockingQueue
基于链表实现。 - 是否有界:
ArrayBlockingQueue
是有界队列,必须在创建时指定容量大小。LinkedBlockingQueue
创建时可以不指定容量大小,默认是Integer.MAX_VALUE
,也就是无界的。但也可以指定队列大小,从而成为有界的。 - 锁是否分离:
ArrayBlockingQueue
中的锁是没有分离的,即生产和消费用的是同一个锁;LinkedBlockingQueue
中的锁是分离的,即生产用的是putLock
,消费是takeLock
,这样可以防止生产者和消费者线程之间的锁争夺。 - 内存占用:
ArrayBlockingQueue
需要提前分配数组内存,而LinkedBlockingQueue
则是动态分配链表节点内存。这意味着,ArrayBlockingQueue
在创建时就会占用一定的内存空间,且往往申请的内存比实际所用的内存更大,而LinkedBlockingQueue
则是根据元素的增加而逐渐占用内存空间。
ArrayBlockingQueue 和 ConcurrentLinkedQueue 有什么区别?
ArrayBlockingQueue
和 ConcurrentLinkedQueue
是 Java 并发包中常用的两种队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别:
- 底层实现:
ArrayBlockingQueue
基于数组实现,而ConcurrentLinkedQueue
基于链表实现。 - 是否有界:
ArrayBlockingQueue
是有界队列,必须在创建时指定容量大小,而ConcurrentLinkedQueue
是无界队列,可以动态地增加容量。 - 是否阻塞:
ArrayBlockingQueue
支持阻塞和非阻塞两种获取和新增元素的方式(一般只会使用前者),ConcurrentLinkedQueue
是无界的,仅支持非阻塞式获取和新增元素。
ArrayBlockingQueue 的实现原理是什么?
ArrayBlockingQueue
的实现原理主要分为以下几点:
ArrayBlockingQueue
内部维护一个定长的数组用于存储元素。- 通过使用
ReentrantLock
锁对象对读写操作进行同步,即通过锁机制来实现线程安全。 - 通过
Condition
条件通知实现线程间的等待和唤醒操作。
这里再详细介绍一下线程间的等待和唤醒具体的实现(不需要记具体的方法,面试中回答要点即可):
-
当队列已满时,生产者线程会调用
notFull.await()
方法让生产者进行等待,等待队列非满时插入(非满条件)。 -
当队列为空时,消费者线程会调用
notEmpty.await()
方法让消费者进行等待,等待队列非空时消费(非空条件)。 -
当有新的元素被添加时,生产者线程会调用
notEmpty.signal()
方法唤醒正在等待消费的消费者线程。 -
当队列中有元素被取出时,消费者线程会调用
notFull.signal()
方法唤醒正在等待插入元素的生产者线程。
Condition
是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能,也就是在一个Lock
对象中可以创建多个Condition
实例(即对象监视器),线程对象可以注册在指定的Condition
中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。
PriorityBlockingQueue
PriorityBlockingQueue
是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 compareTo()
方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator
来指定排序规则。
PriorityBlockingQueue
并发控制采用的是可重入锁 ReentrantLock
,队列为无界队列。(ArrayBlockingQueue
是有界队列,LinkedBlockingQueue
也可以通过在构造函数中传入 capacity
指定队列最大的容量变成有界队列,但是 PriorityBlockingQueue
只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容)。
简单地说,它就是 PriorityQueue
的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException
异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。
LinkedHashMap 和 HashMap 遍历性能比较
LinkedHashMap
维护了一个双向链表来记录数据插入的顺序,因此遍历的时候,是按照双向链表的路径进行遍历的,复杂度与元素的个数成正比。而HashMap的遍历是需要遍历整个哈希表。
什么是 LinkedHashMap?
LinkedHashMap
是 Java 集合框架中 HashMap
的一个子类,它继承了 HashMap
的所有属性和方法,并且拥有顺序插入和访问有序的特性。
特点:
1、支持遍历时会按照插入顺序有序进行迭代。
2、支持按照元素访问顺序排序,适用于封装 LRU 缓存工具。
3、因为内部使用双向链表维护各个节点,所以遍历时的效率和元素个数成正比,相较于和容量成正比的 HashMap 来说,迭代效率会高很多。
LinkedHashMap 如何按照插入顺序迭代元素?
LinkedHashMap
按照插入顺序迭代元素是它的默认行为。LinkedHashMap
内部维护了一个双向链表,用于记录元素的插入顺序。因此,当使用迭代器迭代元素时,元素的顺序与它们最初插入的顺序相同
LinkedHashMap 如何按照访问顺序迭代元素?
LinkedHashMap
可以通过构造函数中的 accessOrder
参数指定按照访问顺序迭代元素。当 accessOrder
为 true 时,每次访问一个元素时,该元素会被移动到链表的末尾,因此下次访问该元素时,它就会成为链表中的最后一个元素,从而实现按照访问顺序迭代元素。
LinkedHashMap 如何实现 LRU 缓存?
将 accessOrder
设置为 true 并重写 removeEldestEntry
方法当链表大小超过容量时返回 true,使得每次访问一个元素时,该元素会被移动到链表的末尾。一旦插入操作让 removeEldestEntry
返回 true 时,视为缓存已满,LinkedHashMap
就会将链表首元素移除,由此我们就能实现一个 LRU 缓存。
LinkedHashMap 和 HashMap 有什么区别?
LinkedHashMap
和 HashMap
都是 Java 集合框架中的 Map 接口的实现类。它们的最大区别在于迭代元素的顺序。HashMap
迭代元素的顺序是不确定的,而 LinkedHashMap
提供了按照插入顺序或访问顺序迭代元素的功能。此外,LinkedHashMap
内部维护了一个双向链表,用于记录元素的插入顺序或访问顺序,而 HashMap
则没有这个链表。因此,LinkedHashMap
的插入性能可能会比 HashMap
略低,但它提供了更多的功能并且迭代效率相较于 HashMap
更加高效。
谈一下什么是CopyOnWriteArrayList ?
JDK1.5 引入了 Java.util.concurrent
(JUC)包,其中提供了很多线程安全且并发性能良好的容器,其中唯一的线程安全 List
实现就是 CopyOnWriteArrayList
。因为,读取操作不会对原有数据进行修改,因此,对于每次读取都进行加锁其实是一种资源浪费,所以我们应该允许多个线程同时访问 List
的内部数据,毕竟对于读取操作来说是安全的。
CopyOnWriteArrayList中的读取操作是完全无需加锁的。更加厉害的是,写入操作也不会阻塞读取操作,只有写写才会互斥,CopyOnWriteArrayList
线程安全的核心在于其采用了 写时复制(Copy-On-Write) 的策略。
即:当需要修改( add
,set
、remove
等操作) CopyOnWriteArrayList
的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。
比如插入操作
// 插入元素到 CopyOnWriteArrayList 的尾部
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// 获取原来的数组
Object[] elements = getArray();
// 原来数组的长度
int len = elements.length;
// 创建一个长度+1的新数组,并将原来数组的元素复制给新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 元素放在新数组末尾
newElements[len] = e;
// array指向新数组
setArray(newElements);
return true;
} finally {
// 解锁
lock.unlock();
}
}
DelayQueue 的实现原理是什么?
DelayQueue
底层是使用优先队列 PriorityQueue
来存储元素,而 PriorityQueue
采用二叉小顶堆的思想确保值小的元素排在最前面,这就使得 DelayQueue
对于延迟任务优先级的管理就变得十分方便了。同时 DelayQueue
为了保证线程安全还用到了可重入锁 ReentrantLock
,确保单位时间内只有一个线程可以操作延迟队列。最后,为了实现多线程之间等待和唤醒的交互效率,DelayQueue
还用到了 Condition
,通过 Condition
的 await
和 signal
方法完成多线程之间的等待唤醒。
DelayQueue 的实现是否线程安全?
DelayQueue
的实现是线程安全的,它通过 ReentrantLock
实现了互斥访问和 Condition
实现了线程间的等待和唤醒操作,可以保证多线程环境下的安全性和可靠性。
DelayQueue 的使用场景有哪些?
DelayQueue
通常用于实现定时任务调度和缓存过期删除等场景。
在定时任务调度中,需要将需要执行的任务封装成延迟任务对象,并将其添加到 DelayQueue
中,DelayQueue
会自动按照剩余延迟时间进行升序排序(默认情况),以保证任务能够按照时间先后顺序执行。
对于缓存过期这个场景而言,在数据被缓存到内存之后,我们可以将缓存的 key 封装成一个延迟的删除任务,并将其添加到 DelayQueue
中,当数据过期时,拿到这个任务的 key,将这个 key 从内存中移除。
DelayQueue 中 Delayed 接口的作用是什么?
Delayed接口定义了元素的剩余延迟时间(getDelay
)和元素之间的比较规则(该接口继承了 Comparable
接口)。
若希望元素能够存放到 DelayQueue
中,就必须实现 Delayed
接口的 getDelay()
方法和 compareTo()
方法,否则 DelayQueue
无法得知当前任务剩余时长和任务优先级的比较。
DelayQueue 和 Timer/TimerTask 的区别是什么?
DelayQueue
和 Timer/TimerTask
都可以用于实现定时任务调度,但是它们的实现方式不同。DelayQueue
是基于优先级队列和堆排序算法实现的,可以实现多个任务按照时间先后顺序执行;而 Timer/TimerTask
是基于单线程实现的,只能按照任务的执行顺序依次执行,如果某个任务执行时间过长,会影响其他任务的执行。另外,DelayQueue
还支持动态添加和移除任务,而 Timer/TimerTask
只能在创建时指定任务。