小林Coding—Java「三、Java集合面试篇」

三 Java集合面试篇

概念

数组和集合的区别,用过哪些?

数组和集合的区别:

  • 数组是固定长度的数据结构,长度无法改变。而集合是动态长度的数据结构。
  • 数组可以包含基本数据类型和对象,而集合只能包含对象。
  • 数组可以直接访问元素,而集合需要通过迭代器或其他方法访问元素

我用过的Java集合类:

  1. ArrayList:动态数组,实现了List接口,支持动态增长
  2. LinkedList:双向链表,也实现了List接口,支持快速插入和删除。
  3. HashMap:基于哈希表的Map实现,存储键值对,通过键快速查找值。
  4. HashSet:基于HashMap实现的Set集合。用于存储唯一元素
  5. TreeMap:基于红黑树实现的有序Map集合,可以按照键的顺序进行排序。
  6. LinkedHashMap:基于哈希表和双向链表实现的Map集合,保持插入顺序或访问顺序。
  7. PriorityQueue:优先队列,可以按照比较器或元素的自然顺序进行排序。

说说Java中的集合


List是有序的Collection。实现List的类有LinkedList,ArrayList,Vector,Stack。

  • ArrayList是非线程安全列表,底层用数组实现。ArrayList随机访问很快,但插入和删除很慢。
  • LinkedList本质是一个双向链表,与ArrayList相比,插入和删除速度快,但随机访问速度变慢。

Set是去重集合。Set无序,常用的实现有 HashSet,LinkedHashSet,TreeSet。

  • HashSet通过HashMap实现,HashMap的key即HashSet存储的元素。所有的Key都使用相同Value:一个名为PRESENT的Object类型变量。使用Key保证元素唯一性,但不保证有序。HashSet线程不安全
  • LinkedHashSet继承HashSet,通过LinkedHashMap实现,使用双向链表维护元素插入顺序
  • TreeSet通过TreeMap实现,添加元素到集合时按照比较规则将其插入到合适位置,保证插入后集合仍有序。

Map是一个键值对集合,存储键、值和之间的映射。Key无序、唯一;value无序,可重复。

Map没有继承Collection接口。实现有TreeMap,HashMap,HashTable,LinkedHashMap,ConcurrentHashMap。

  • HashMap:长度小于阈值(默认为8)时使用数组+链表实现,大于时将链表转为红黑树,以减少搜索时间。
  • LinkedHashMap:继承自HashMap,所以其底层仍然基于拉链式散列结构,即由数组和链表或红黑树组成。LinkedHashMap还加了一条双向链表,使其可以保持键值对的插入顺序。
  • HashTable:数组+链表组成,数组是HashTable的主体,链表则是主要为了解决哈希冲突而存在的。
  • TreeMap:红黑树(自平衡的排序二叉树)
  • ConcurrentHashMap:Node数组+链表+红黑树实现,是线程安全的(1.8后volatile + CAS 或者synchronized)

Java中线程安全的集合是什么?

在java.util 包中的线程安全的类主要 2 个,其他都是非线程安全的:

  • Vector:其内部方法基本都经过synchronized修饰。Vector内部是使用对象数组来保存数据。当数组已满时,会创建新的数组,并拷贝原有数组数据。如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。
  • Hashtable:线程安全的哈希表。其枷锁方法是给每个方法加上synchronized关键字,这样锁住的是整个Table对象,不支持null键和值。很少使用。如果要保证线程安全的哈希表,可以使用ConcurrentHashMap

java.util.concurrent 包提供的都是线程安全的集合:

并发Map:

  • ConcurrentHashMap:在Jdk1.8,它直接在table元素上加锁,实现对每一行进行加锁,进一步见笑了并发冲突和概率。对于put操作,①如果key对应的数组元素为null,则通过CAS(Compare and Swap)将其设置为当前值。②如果key对应的数组元素不为null,则对该元素使用synchronized关键字申请锁,从而提高寻址效率。
  • ConcurrentSkipListMap:实现了一个基于SkipList(跳表)算法的可排序的并发集合,SkipList是一种可以在对数预期时间内完成搜索、插入和删除熬做的数据结构,是通过维护多个指向其他元素的跳跃链接来实现高校查找。

并发Set:

  • ConcurrentSkipListSet:线程安全的有序集合。底层使用ConcurrentSkipListMap。
  • CopyOnWriteArraySet:是线程安全的HashSet。CopyOnWriteArraySet和HashSet都继承共同的父类AbstractSet,但HashSet是通过散列表实现,CopyOnWriteArraySet通过动态数组(CopyOnWriteArrayList)实现的,并不是散列表。

并发List:

  • CopyOnWriteArrayList:它是ArrayList的线程安全的变体,当对象进行写操作时,使用Lock锁做同步处理,同步拷贝了原数组,并在新数组上进行添加操作,最后将新数组替换旧数组。若读操作,则直接返回结果,操作过程不需要进行同步处理。

并发Queue:

  • ConcurrentLinkedQueue:是一个适用于高并发场景下的队列,它通过无锁的方式(CAS),实现了高并发状态下的高性能。
  • BlockingQueue:主要用于简化多线程间的数据共享。BlockingQueue提供一种读写阻塞等待的机制,即如果消费者速度较快则BlockingQueue可能被清空,此时消费线程再试图从BlockingQueue读取数据时会被阻塞。反之如果生产队列较快,则BlockingQueue可能会被装满,此时生产线程再试图像BlockingQueue队列装入数据时便会被阻塞等待。

并发Deque:

  • LinkedBlockingDeque:线程安全的双端队列。内部使用链表实现,每个节点有前驱节点后继结点。LinkedBlockingDeque没有进行读写锁的分离,因此同一时间只能有一个线程对其操作。
  • ConcurrentLinkedDeque:ConcurrentLinkedDeque是一种基于链表节点的无限并发链表。可以安全地并发执行插入、删除和访问操作。当许多线程访问一个公共集合时,ConcurrentLinkedDeque是一个合适的选择。

Collections和Collection的区别

  • Collection是所有集合类的基础接口,定义了通用的操作,如添加、遍历和删除等。Collection接口有许多实现类,如List,Set和Queue等。
  • Collections是位于java.util包中的工具类。提供了一系列静态方法,用于对集合进行操作和算法。包含排序、查找、替换、反转、随机化等。这些方法可以实现对Collection接口的集合进行操作,如List和Set。

集合遍历的方法有哪些?

在Java中,集合的遍历方法主要有下面几种:

  • 普通 for 循环
java 复制代码
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

for (int i = 0; i < list.size(); i++) {
    String element = list.get(i);
    System.out.println(element);
}
  • 增强 for 循环(for-each循环)
java 复制代码
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

for (String element : list) {
    System.out.println(element);
}
  • Iterator 迭代器: 可以使用迭代器来遍历集合,特别适用于需要删除元素的情况。
java 复制代码
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
    String element = iterator.next();
    System.out.println(element);
}
  • ListIterator 列表迭代器: ListIterator是迭代器的子类,可以双向访问列表并在迭代过程中修改元素。
java 复制代码
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

ListIterator<String> listIterator= list.listIterator();
while(listIterator.hasNext()) {
    String element = listIterator.next();
    System.out.println(element);
}
  • 使用 forEach 方法
java 复制代码
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

list.forEach(element -> System.out.println(element));
  • Stream API: Java 8的Stream API提供了丰富的功能,可以对集合进行函数式操作,如过滤、映射等。
java 复制代码
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

list.stream().forEach(element -> System.out.println(element));

List

把ArrayList变成线程安全有哪些方法?

  • 使用Collections类的synchronizedList方法将ArrayList包装成线程安全的List:
java 复制代码
List<String> synchronizedList = Collections.synchronizedList(arrayList);
  • 使用CopyOnWriteArrayList类代替ArrayList,它是一个线程安全的List实现:
java 复制代码
CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>(arrayList);

使用Vector类代替ArrayList,Vector是线程安全的List实现:

java 复制代码
Vector<String> vector = new Vector<>(arrayList);

ArrayList线程不安全的体现在哪里?

在高并发下,ArrayList会暴露三个问题:

  • 部分值为null
  • 索引越界异常
  • size和我们add的数量不符。

三种情况分别是如何产生的:

  • 部分值为null:当线程1走到了扩容那里发现当前size是9,而数组容量是10,所以不用扩容,这时候cpu让出执行权,线程2也进来了,发现size是9,而数组容量是10,所以不用扩容,这时候线程1继续执行,将数组下标索引为9的位置set值了,还没有来得及执行size++,这时候线程2也来执行了,又把数组下标索引为9的位置set了一遍,这时候两个先后进行size++,导致下标索引10的地方就为null了。
  • 索引越界异常:线程1走到扩容那里发现当前size是9,数组容量是10不用扩容,cpu让出执行权,线程2也发现不用扩容,这时候数组的容量就是10,而线程1 set完之后size++,这时候线程2再进来size就是10,数组的大小只有10,而你要设置下标索引为10的就会越界(数组的下标索引从0开始);
  • size与我们add的数量不符:这个基本上每次都会发生,这个理解起来也很简单,因为size++本身就不是原子操作,可以分为三步:获取size的值,将size的值加1,将新的size值覆盖掉原来的,线程1和线程2拿到一样的size值加完了同时覆盖,就会导致一次没有加上,所以肯定不会与我们add的数量保持一致的;

ArrayList的扩容机制是怎么样的

主要步骤如下:

  1. 计算新的容量:新的容量会扩大为原容量的1.5倍,然后检查是否超过了最大容量限制。
  2. 创建新的数组:根据计算得到的新容量,创建一个新的更大的数组
  3. 将元素复制:将原来数组的元素逐个复制到新数组中。
  4. 更新引用:将ArrayList内部指向原数组的引用指向新数组。
  5. 完成扩容:扩容完成后,可以继续添加新元素。

ArrayList的扩容操作涉及到数组的复制,内存的重新分配,所以在频繁添加大量元素时,扩容操作可能会影响性能。为了减少扩容的性能损耗,可以在初始化ArrayList时预分配足够大的容量,避免频繁触发扩容操作。

扩容1.5倍是因为 1.5 可以充分利用移位操作,减少浮点数或运算时间和运算次数。

java 复制代码
// 新容量计算
int newCapacity = oldCapacity + (oldCapacity >> 1);

线程安全的List,CopyonWriteArraylist 是如何实现线程安全的?

CopyonWriteArraylist 底层也是通过一个数组保存数据,使用volatile关键字修饰数组,保证当前线程对数组对象重新赋值后,其他线程可以及时感知到:

java 复制代码
private transient volatile Object[] array;

在写操作,加入了一把互斥锁 ReentrantLock 以保证线程安全。

在读操作是没有加锁的,所以读是一直都能读。

Map

HashMap的原理

JDK 1.7之前,HashMap的数据结构是数组和链表,通过哈希算法将元素的键 key 映射到数组中的槽位(bucket)。如果冲突,会以链表的形式存储在一个槽位上。

在 JDK 1.8 版本后,当一个链表的长度超过 8 时,将链表转为红黑树,查找为O(logn)。然后在数量小于6时,将红黑树转回链表。

哈希冲突解决方法有哪些?

  • 链接法
  • 开放寻址法:包含线性探测、二次探测和双重散列。
  • 再哈希法
  • 哈希桶扩容:当哈希冲突过多时,可以动态地扩大哈希桶的数量,重新分配键值对以减少冲突的概率。

HashMap是线程安全的吗?

  • 数组+链表存储,多线程背景下数组扩容时,存在 Entry 链死循环和数据丢失问题。
  • 数组+链表+红黑二叉树,多线程背景下,put方法存在数据覆盖的问题。

要保证线程安全,可以通过这些方法来保证:

  • 使用Collections.synchronizedMap同步加锁方式,还可以使用ConcurrentHashMap。
  • ConcurrentHashMap在JDK1.7使用Segment+HashEntry分段锁的方式实现,1.8则改为用CAS+synchronized+Node实现,同样加入了红黑树,以避免链表过长导致的性能问题。

HashMap的put过程介绍一下

  1. 根据要添加的键计算哈希,得到这数组中的索引。
  2. 检查该位置是否为空(有无键值对存在)
  • 如果为空,则直接在该位置创建一个新的Entry对象来存储键值对。将HashMap的修改次数(modCount)加1,以便在进行迭代时发现和并发修改。
  1. 如果该位置已存在键值对,检查该位置第一个键值对的哈希码和键是否与要添加的键值对相同?
  • 如果相同,则直接将新的值替换旧的值(键都是相同的),完成更新动作。
  1. 如果第一个键值对的哈希码和键不同,则需要遍历链表或红黑树来潮找是否有相同的键:
  • 是链表:则从头部逐个比较键的哈希玛和equals()方法,直到找到相同的键或到达链表末尾。
    • 找到相同的键:用新值代替旧值,更新键对应的值。
    • 找不到相同的键:则将新的键值对添加到链表头
  • 是红黑树:在红黑树中使用哈希码和equals()方法进行查找。根据键的哈希码,定位到某个节点,然后逐个比较键,直到找到相同的键或到达红黑树的末端。
  1. 检查链表长度是否到达阈值 8
  • 如果链表长度超过8,且HashMap的数组长度大于等于64,则会将链表转为红黑树。
  1. 检查负载因子是否超过阈值(默认为0.75)
  • 如果键值对的数量(size)和数组的长度的比值大于阈值,则需要进行扩容操作。
  1. 扩容操作:
  • 创建一个新的 2 倍大小的数组
  • 将旧数组中的键值对重新计算哈希码并分配到新数组。
  • 更新HashMap的数组引用和阈值参数。
  1. 完成添加操作。

HashMap调用get方法一定安全吗?为什么?

  • 空指针异常:如果HashMap没有被初始化,则会抛出空指针异常。反之则使用null作为键是允许的,因为HashMap支持null。
  • 线程安全:HashMap本身不是线程安全的。如果在多线程中用HashMap,可以用ConcurrentHashMap。

HashMap为什么一般用String作为key?

用String做key,是因为String对象是不可变的,一旦创建就不能被修改,保证了key的稳定性。

为什么HashMap要用红黑树而不是平衡二叉树?

  • 平衡二叉树过于严格,任何节点的左右字数高度差不会超过1。但是每次插入/删除时,几乎都会破坏规则,所以都需要通过左旋 右旋来调整。
  • 红黑树追求一种弱平衡:整个树最长路径不会长过最短路径的 2 倍。和平衡二叉树不同的是,红黑树在插入、删除等操作时,不会像平衡二叉树那样频繁破坏规则,所以不需要频繁调整,这也是我们大多数情况下用红黑树的原因。

HashMap的key可以为null吗?

可以为null。

  • hashMap使用hash()方法来计算key的哈希值,当key为null时,直接令key的哈希值是0,不走key.hashCode()方法:
  • hashMap虽然支持key和value为null,但是null作为key只能有一个,null作为value可以有多个;
  • 因为hashMap中,如果key值一样,那么会覆盖相同key值的value为最新,所以key为null只能有一个。

重写HashMap的equal和hashcode方法需要注意什么?

从HashMap中取值时,要用到key对象的hashCode()和equals方法。

此外,所有不允许重复存储数据的集合类都使用hashCode()和equals()去查找重复。equals()和hashCode()的实现应遵循如下规则:

  • 如果o1.equals(o2),则o1.hashCode() == o2.hashCode()总为true。
  • 如果o1.hashCode() == o2.hashCode(),并不一定o1.equals(o2)成立。

重写HashMap的equal方法不当会出现什么问题?

HashMap在比较元素时,会先通过hashCode进行比较,相同的情况下再通过equals进行比较。所以 equals相等的两个对象,hashCode一定相等。hashCode相等的两个对象,equals不一定相等(比如散列冲突的情况)

重写了equals方法,不重写hashCode方法时,可能会出现equals方法返回为true,而hashCode方法却返回false,这样的一个后果会导致在hashmap等类中存储多个一模一样的对象,导致出现覆盖存储的数据的问题,这与hashmap只能有唯一的key的规范不符合。

列举HashMap在多线程中存在的问题:

  • JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。
  • 多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在JDK 1.7和 JDK 1.8 中都存在。

HashMap的扩容机制介绍一下(不懂⭐️)

hashMap默认的负载因子是0.75,即如果hashmap中的元素个数超过了总容量75%,则会触发扩容,扩容分为两个步骤:

  1. 对哈希表长度的扩展(2倍)
  2. 是将旧哈希表中的数据放到新的哈希表中。

因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。

因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成"原索引+oldCap"。可以看看下图为16扩充为32的resize示意图:

HashMap的大小为什么是2的n次方大小呢?(不懂⭐️)

而在 JDK 1.8 中,HashMap 对扩容操作做了优化。由于扩容数组的长度是 2 倍关系,所以对于假设初始 tableSize = 4 要扩容到 8 来说就是 0100 到 1000 的变化(左移一位就是 2 倍),在扩容中只用判断原来的 hash 值和左移动的一位(newtable 的值)按位与操作是 0 或 1 就行,0 的话索引不变,1 的话索引变成原索引加上扩容前数组。(旧哈希值和新哈希值右边的位数都完全一样。)

往hashmap存20个元素,会扩容几次?

初始容量:16

插入第 1 到第 12 个元素时,不需要扩容。

插入第 13 个元素时,达到负载因子限制,需要扩容。此时,HashMap 的容量从 16 扩容到 32。

扩容后的容量:32

插入第 14 到第 24 个元素时,不需要扩容。

因此,总共会进行一次扩容。

Hashmap和Hashtable有什么不一样的?Hashmap一般怎么用?

  • HashMap线程不安全,效率高一点,可以存储null的key和value,null的key只能有一个,null的value可以有多个。默认初始容量为16,每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2的幂次方大小。底层数据结构为数组+链表,插入元素后如果链表长度大于阈值(默认为8),先判断数组长度是否小于64,如果小于,则扩充数组,则链表转化为红黑树,以减少搜索时间。如果负载因子大于0.75,则扩容。
  • HashTable线程安全,效率低一点,其内部方法基本都经过synchronized修饰,不可以有null的key和value。默认初始容量为11,每次扩容变为原来的2n+1。创建时给定了初始容量,会直接用给定的大小。底层数据结构为数组+链表。它基本被淘汰了,要保证线程安全可以用ConcurrentHashMap。

ConcurrentHashMap怎么实现的?

  • JDK 1.7 ConcurrentHashMap
    使用数组+链表形式实现的。数组又分为:大数组Segment和小数组HashEntry。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap中扮演锁的角色。
    HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组 (多个Segment元素),一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构元素。

JDK1.7ConcurrentHashMap 分段锁技术将数据分成一段段存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段数据也能被其他线程访问,能够实现真正的并发访问。

  • JDK 1.8 ConcurrentHashMap
    JDK1.8使用数组+链表/红黑树的方式优化ConcurrentHashMap,优化数据比较多情况下访问慢的情况。

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

如果把上面的一句话归纳,就是ConcurrentHashMap通过对头节点(根节点)加锁来保证线程安全,锁的粒度相比Segment来说更小了,发生冲突和加锁的频率降低了。并发操作的性能就提高了。

分段锁怎么加锁的?

在 ConcurrentHashMap 中,将整个数据结构分为多个 Segment,每个 Segment 都类似于一个小的 HashMap,每个 Segment 都有自己的锁,不同 Segment 之间的操作互不影响,从而提高并发性能。

在 ConcurrentHashMap 中,对于插入、更新、删除等操作,需要先定位到具体的 Segment,然后再在该 Segment 上加锁,而不是像传统的 HashMap 一样对整个数据结构加锁。这样可以使得不同 Segment 之间的操作并行进行,提高了并发性能。

分段锁是可重入的吗?

JDK 1.7 ConcurrentHashMap中的分段锁是用了 ReentrantLock,是一个可重入的锁。

什么是可重入 :可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁

换言之:可重入就是一个线程不用释放,可以重复的获取一个锁n次,只是在释放的时候,也需要相应的释放n次。(简单来说:A线程在某上下文中或得了某锁,当A线程想要在次获取该锁时,不会应为锁已经被自己占用,而需要先等到锁的释放)假使A线程即获得了锁,又在等待锁的释放,就会造成死锁。)

什么是悲观锁和乐观锁?

  1. 悲观锁:
    顾名思义,就是比较悲观的锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
    悲观锁的实现方式为:synchronizedReentrantLock
  2. 乐观锁
    总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。
    乐观锁实现方式:版本号控制CAS算法
    CAS算法是compare and swap(比较和交换)

ConcurrentHashMap用了悲观锁还是乐观锁?

添加元素时首先会判断容器是否为空。

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

HashTable底层实现原理是什么?

  • Hashtable的底层数据结构主要是数组加链表。数组是主体,链表是解决hash冲突存在的。
  • HashTable是线程安全的,实现方式是Hashtable的所有公共方法均采用synchronized关键字(悲观锁)。当一个线程访问同步方法,另一个线程也访问的时候,就会陷入阻塞或轮询的状态。

HashTable线程安全是怎么实现的?

可以看到,Hashtable是通过使用了 synchronized 关键字来保证其线程安全。

在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。

hashtable和concurrentHashMap的区别

底层数据结构:

  • jdk7之前concurrenthashmap底层采用分段数组+链表 实现,jdk8后采用的是数组+链表/红黑树
  • HashTable采用数组+链表,数组是主体,链表是解决hash冲突存在的 。

实现线程安全的方式:

  • jdk8前,concurrenthashmap采用分段锁,对整个数组进行了分段分割,每一把锁只锁容器里的一部分数据,多线程访问不同数据段里的数据就不会存在锁竞争,提高并发访问。jdk8后,直接采用数组+链表/红黑树,并发控制采用CAS和synchronized操作,更加提高了速度。
  • Hashtable:所有的方法都加了悲观锁来控制。当一个线程访问同步方法,此时另一个方法也来访问时,就会陷入阻塞或者轮询的状态。

说一下HashMap和Hashtable、ConcurrentMap的区别 (不熟⭐️⭐️)

  • HashMap线程不安全,效率高一点,可以存储null的key和value,null的key只能有一个,null的value可以有多个。默认初始容量为16,每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2的幂次方大小。底层数据结构为数组+链表,插入元素后如果链表长度大于阈值(默认为8),先判断数组长度是否小于64,如果小于,则扩充数组,反之将链表转化为红黑树,以减少搜索时间。
  • HashTable线程安全,效率低一点,其内部方法基本都经过synchronized修饰,不可以有null的key和value。默认初始容量为11,每次扩容变为原来的2n+1。创建时给定了初始容量,会直接用给定的大小。底层数据结构为数组+链表。它基本被淘汰了,要保证线程安全可以用ConcurrentHashMap。
  • ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它可以在多线程环境下并发地进行读写操作,而不需要像传统的HashTable那样在读写时加锁。ConcurrentHashMap的实现原理主要基于分段锁和CAS操作。它将整个哈希表分成了多Segment(段),每个Segment都类似于一个小的HashMap,它拥有自己的数组和一个独立的锁。在ConcurrentHashMap中,读操作不需要锁,可以直接对Segment进行读取,而写操作则只需要锁定对应的Segment,而不是整个哈希表,这样可以大大提高并发性能。

Set

Set集合有什么特点?如何实现key无重复的?

  • set集合特点:Set集合中的元素是唯一的,不会出现重复的元素。
  • set实现原理:Set集合通过内部的数据结构(如哈希表、红黑树等)来实现key的无重复。当向Set集合中插入元素时,会先根据元素的hashCode值来确定元素的存储位置,然后再通过equals方法来判断是否已经存在相同的元素,如果存在则不会再次插入,保证了元素的唯一性

有序的Set是什么?记录插入顺序的集合是什么?

  • 有序的 Set 是TreeSet和LinkedHashSet 。TreeSet是基于红黑树实现,保证元素的自然顺序。LinkedHashSet是基于双重链表和哈希表的结合来实现元素的有序存储,保证元素添加的顺序
  • 记录插入顺序的集合通常指的是LinkedHashSet,它不仅保证元素的唯一性,还可以保持元素的插入顺序。当需要在Set集合中记录元素的插入顺序时,可以选择使用LinkedHashSet来实现。
相关推荐
ZJ_.1 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
Narutolxy7 分钟前
深入探讨 Go 中的高级表单验证与翻译:Gin 与 Validator 的实践之道20241223
开发语言·golang·gin
Hello.Reader14 分钟前
全面解析 Golang Gin 框架
开发语言·golang·gin
禁默25 分钟前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程
Cachel wood31 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
Code哈哈笑34 分钟前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
gb421528737 分钟前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端
程序猿进阶37 分钟前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
qq_4336184440 分钟前
shell 编程(二)
开发语言·bash·shell