JAVA中Map(ConcurrentHashMap)、List、Queue、Stack面试题终结者

1.Collection接口

2.List

2.1概述

List是有序的可重复的集合,可以在任意位置增加删除元素,用Iterator实现单向遍历(从前到后遍历集合),也可用ListIterator实现双向遍历(从前到后和从后向前遍历集合)。

package com.list;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class demo4_list {
    public static void main(String[] args) {
         List l=new ArrayList();
         l.add("a");
         l.add("b");
         l.add("abc");
         System.out.println("正序输出:");
         ListIterator lit = l.listIterator();
         while(lit.hasNext()){
             System.out.println(lit.next());      //获取元素并将指针向后移动
         }
         System.out.println("倒序输出:");

         while(lit.hasPrevious()){
             System.out.println(lit.previous());  //获取元素并将指针向前移动
         }
    }
}

List常用方法:add()/get()/remove()/size()/contains()/addFirst()/addLast()/removeFirst()/removeLast()/getFirst()/getLast()等。

2.2Vector和ArrayList、LinkedList联系和区别?分别的使用场景?

(1)联系区别:

ArrayList:底层是数组实现,线程不安全,查询和修改快,新增和删除慢

LinkedList:底层是双向链表,线程不安全,查询和修改慢,新增和删除快

Vector:底层是数组,使用了synchronized对add()等方法加锁,线程安全。

ArrayList和Vector底层是数组(在内存中分配连续的空间),数组的查询和修改是根据下标操作所以快,但是新增和删除需要移动元素等内存操作所以比较慢,数组长度大于实际存储的元素数以便插入元素。LinkedList在插入删除数据时只需要记录本项的前后项即可所以比较快。

ArrayList初始化容量为0,扩容调用grow(),grow()每次扩容1.5倍,1.8之前初始化容量为10。

(2)使用场景:

多线程场景用Vector。
添加和删除多的场景用LinkedList。
查询和修改多的场景用ArrayList。

2.3如果需要保证ArrayList的线程安全需要怎么做?有几种方法?

2.2.1保证ArrayList的线程安全需要怎么做?有几种方法?

  1. 自己写一个包装类,继承ArrayList,使用synchronized同步所有ArrayList方法,根据业务一般是add/update/remove加锁。
  2. List list = Collections.synchronizedList(new ArrayList<>());底层也是在集合的所有方法之上加上了synchronized。
  3. CopyOnWriteArrayList list = new CopyOnWriteArrayList();使用ReentrantLock加锁。

2.2.2追问:了解CopyOnWriteArrayList吗?和Collections.synchronizedList实现线程安全有什么区别,使用场景是什么?

  1. CopyOnWriteArrayList:执行增删改操作,会拷贝一份新的数据(add/set/remove),开销比较大,修改后会将原来的集合指向新的集合,使用ReentrantLock来保证多个线程安全

  • 场景:适合于读多写少的场景(读操作不需要加锁,添加删除和修改需要加锁)
  1. Collections.synchronizedList:线程安全是因为每个方法都加了synchronized同步锁。
  • 场景:适合写多读少的场景(写操作比CopyOnWriteArrayList好,但是读操作不如CopyOnWriteArrayList)

2.2.3追问:CopyOnWriteArrayList的设计思想是什么?有什么缺点?

设计思想:读写分离+最终一致。缺点:内存占用问题,由于写时复制,内存里同时存在多个对象的内存占用。注释:Copy On Write也是一种重要的思想,简称COW,在写少读多的情况下使用,如果不是写少读多的场景,使用CopyOnWriteArrayList 开销比较大,每次更新操作(add/set/remove)都会做一次数组copy。

2.2.4追问:什么是COW?

如果多个用户同时访问相同资源,他们会获取到指向相同的资源相同的指针,直到某个用户修改资源的内容时,系统才会真正复制一份专用的副本给该用户,而其它用户所见的最初的资源仍然保持不变。优点是如果用户没有修改该资源,就不会有副本被创建,因此多个用户只是读取操作时可以共享同一份资源。

2.2.5追问:CopyOnWriteArrayList的特点?

①CopyOnWriteArrayList是线程安全的容器(相对于ArrayList),底层通过复制数组的方式实现。
②CopyOnWriteArrayList在遍历的时候不会抛出ConcurrentModificationException异常,并且遍历的
时候不用额外加锁

③元素可以为null。

3.Set

Set是无序的不可重复集合,set 中最多包含一个 null 元素,只能用 Iterator 迭代器实现单项遍历, Set 中没有同步方法

4.Map

4.1概述

对于频繁的遍历操作,LinkedHashMap执行效率高于HashMap。

HashMap是通过链地址法解决hash冲突的问题,ThreadLocalMap是通过开放地址法来解决hash冲突的问题。链地址法

//HashMap默认初始容量:16,可以指定初始化容量,指定3实际会为4(3在2的1-2次幂之间取2的2次幂),指定4实际会为4,指定5实际会为8

4.2红黑树知识点补充

【数据结构】史上最好理解的红黑树讲解,让你彻底搞懂红黑树_小七mod的博客-CSDN博客狂肝半个月的学习笔记,最通俗易懂的红黑树讲解。带你快速掌握红黑树~https://blog.csdn.net/cy973071263/article/details/122543826?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168238778816800222874220%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=168238778816800222874220&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-122543826-null-null.142%5Ev86%5Econtrol,239%5Ev2%5Einsert_chatgpt&utm_term=%E7%BA%A2%E9%BB%91%E6%A0%91&spm=1018.2226.3001.4187

4.2.1红黑树(自平衡二叉查找树)、平衡二叉树(AVLTree)

红黑树(Red Black Tree)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作,其中N代表输入数据的量,O描述的是算法的运行时间和输入数据之间的关系。

平衡二叉树(AVLTree),其实平衡二叉树最大的作用就是查找,AVL树的查找、插入和删除在平均和最坏情况下都是O(logN)。AVL树的效率就是高在这个地方。如果在AVL树中插入或删除节点后,使得高度之差大于1。此时,AVL树的平衡状态就被破坏,它就不再是一棵平衡二叉树;为了让它重新维持在一个平衡状态,就需要对其进行旋转处理, 那么创建一颗平衡二叉树的成本其实不小。 这个时候就有人开始思考,并且提出了红黑树的理论,红黑树在业界应用很广泛,比如 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map 均是基于红黑树结构实现的。

何为旋转?

旋转操作分为左旋和右旋,右旋是将某个节点旋转为其左孩子的右孩子,而左旋是将某个节点旋转为其右孩子的左孩子。这话听起来有点绕,所以还是请看下图:

4.2.2红黑树到底比AVL树好在哪里?

平衡二叉树,它的操作效率(查询,插入,删除)较高,时间复杂度是O(logN)。但是可能会出现一种极端的情况,那就是插入的数据是有序的(递增或者递减),那么所有的节点都会在根节点的右侧或左侧,此时,二叉搜索树就变为了一个链表,它的操作效率就降低了,时间复杂度为O(N),所以可以认为平衡二叉搜索树的时间复杂度介于O(logN)和O(N)之间,视情况而定。那么为了应对这种极端情况,红黑树就出现了,它是具备了某些特性的自平衡二叉搜索树,能解决非平衡树问题,红黑树是一种接近平衡的二叉树(说它是接近平衡因为它并没有像AVL树即平衡二叉树的平衡因子的概念,它只是靠着满足红黑树的3个关键特征来维持一种接近平衡的结构,进而提升整体的性能,并没有严格的卡定某个平衡因子来维持绝对平衡)。

1】红黑树的查找/插入/删除操作,时间复杂度都是O(logN)

2】查找操作时,它和普通的相对平衡的二叉搜索树的效率相同,都是通过相同的方式来查找的,没有用到红黑树的特性。但如果插入的时候是有序数据,那么红黑树的查询效率就比二叉搜索树要高了,因为此时二叉搜索树不是平衡树而是链表,它的时间复杂度是O(N)

3】插入和删除操作时,由于红黑树的每次操作平均要旋转一次和变换颜色,所以它比普通的二叉搜索树效率要低一点,不过时间复杂度仍然是O(logN)。

4】总之,红黑树相比平衡二叉树的优点就是对有序数据的查询操作不会慢到O(logN)的时间复杂度

4.2.3红黑树的3个关键特征

  1. 节点是红色或黑色、根是黑色、叶子节点都是黑色(这里的叶子节点指的是最底层的空节点,NULL节点的父节点在红黑树里不将其看作叶子节点)。
  2. 红色节点的子节点都是黑色、红色节点的父节点都是黑色,从根节点到叶子节点的所有路径上不能有 2 个连续的红色节点
  3. 从任一节点到叶子节点(即NULL节点)的所有路径都包含相同数目的黑色节点

注意下图就不是一个红黑树(不满足第3点,路径1有4个黑色节点,路径2有3个黑色节点):

4.2.4什么是n阶B树

  1. 1<=根结点元素数量<=n-1
  2. ceil(n/2)-1<=非根结点元素数量<=n-1
  3.  A节点的子节点个数=A结点内元素数量+1(A为任意节点)

由3得出:

  1. 2<=根节点的子节点个数<=n
  2. ceil(n/2)<=非根节点的子节点个数<=n

所以为了一目了然,一般3阶B树也叫2-3树,4阶B树也叫2-4树,5阶B树也叫3-5树。其中的数字代表非根节点的子节点数量范围/分支条数

4.3ConcurrentHashMap

ConcurrentHashMap原理详解(太细了)_笑我归无处的博客-CSDN博客和HashMap一样,是一个存放键值对的容器。使用hash算法来获取值的地址,因此时间复杂度是O(1)。查询非常快。同时,是线程安全的HashMap。专门用于多线程环境。做插入操作时,首先进入乐观锁,然后,在乐观锁中判断容器是否初始化,如果没初始化则初始化容器,如果已经初始化,则判断该hash位置的节点是否为空,如果为空,则通过CAS操作进行插入。如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则帮助其扩容。如果没有扩容,则进行最后一步,先加锁,然后找到hash。......https://blog.csdn.net/qq_42068856/article/details/126091526

4.3.1什么是ConcurrentHashMap

ConcurrentHashMapHashMap一样,是一个存放键值对的容器。使用hash算法来获取值的地址,因此查询时间复杂度是O(1),查询非常快
同时,ConcurrentHashMap是线程安全的HashMap,专门用于多线程环境。

4.3.2ConcurrentHashMap和Hashtable的区别

HashTable是线程安全的,但是HashTable只是单纯的在add()方法上加上synchronized,保证插入时阻塞其他线程的插入操作。虽然安全,但因为设计简单,所以性能低下。

ConcurrentHashMap是线程安全的,ConcurrentHashMap并非锁住整个方法,而是通过局部加锁的方式保证多线程的线程安全,减少了性能损耗。

4.3.3ConcurrentHashMap原理

HashTable类继承自Dictionary类、实现了Map接口。 大部分的操作都是通过synchronized锁保护的,是线程安全的,key、value都不可以为null,每次put方法不允许null值,否则直接抛出异常。它的数据结构:数组+单向链表HashTable、HashMap无序的,TreeMap有序的。另外,官方文档也说了:如果在线程安全的情况下使用,建议使用HashMap替换,如果在非线程安全的情况下使用,建议使用ConcurrentHashMap替换。

JDK1.7版本:

容器中有多把锁,每一把锁锁一段数据,这样在多线程访问不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想,见下图:
在这里插入图片描述

数据结构:数组+单向链表

在这里插入图片描述

JDK1.8版本:做了2点修改,见下图:

取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,并发控制使用Synchronized + CAS来实现。将原先数组+单向链表的数据结构,变更为数组+单向链表+红黑树的结构

在这里插入图片描述

4.3.4源码解读

①volatile修饰的节点数组

//ConcurrentHashMap使用volatile修饰节点数组,保证其可见性,禁止指令重排。
transient volatile Node<K,V>[] table;
//HashMap没有用volatile修饰节点数组。
transient Node<K,V>[] table;

②ConcurrentHashMap的put()方法--Synchronized + CAS

总结
做插入操作时,首先进入乐观锁,
然后,在乐观锁中判断容器是否初始化,
如果没初始化则初始化容器,
如果已经初始化,则判断该hash位置的节点是否为空,如果为空,则通过CAS操作进行插入。
如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则先帮助其扩容。
如果没有扩容则进行最后一步,先加Synchronized排他锁,然后找到hash值相同的节点(hash冲突),循环判断这个节点上的链表,决定做覆盖操作还是插入操作。

③ConcurrentHashMap的get()方法

//ConcurrentHashMap的get()方法是不加锁的,方法内部也没加锁。
public V get(Object key)

ConcurrentHashMapget()方法是不加锁的,为什么可以不加锁?

//ConcurrentHashMap使用volatile修饰节点数组,保证其可见性,禁止指令重排。
transient volatile Node<K,V>[] table;

因为table数组volatile关键字修饰,保证每次获取值都是最新的。

5.Queue

Queue遵从先进先出原则,使用时尽量避免add()和remove()方法,而是使用offer()来添加元素、使用poll()来移除元素、优点是可以通过返回值来判断是否成功,LinkedList 实现了Queue接口Queue 通常不允许插入 null 元素,尽管某些实现(例如 LinkedList )不禁止插入null

6.Stack

Stack遵从后进先出原则,继承自Vector,它通过五个操作对Vector类进行扩展允许将向量视为堆栈,它提供了push(把元素压入堆栈顶部)pop(移除堆栈顶部的对象并作为此函数的返回值)操作,以及查看栈顶点的peek()方法测试栈是否为空的empty()方法返回对象在栈中位置的search(Object element)方法(以1为基数)等方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值