文章目录
1.List、Set、Map三者的区别? 三者底层的数据结构?
List、Set、Map 是 Java 中常用的集合类型,它们在数据结构和用途上有所不同。
List(列表):
- List 是一个有序的集合,允许重复元素。
- 可以通过索引来访问列表中的元素,元素的位置是有意义的。
- 常见的实现类包括 ArrayList、LinkedList、Vector 等。
- ArrayList 底层基于数组实现,LinkedList 底层基于链表实现,Vector 也是基于数组实现,但线程安全。
Set(集合):
- Set 是一个不允许重复元素的集合,保证元素的唯一性。
- Set 不保证元素的顺序,元素的排列顺序可能是不确定的。
- 常见的实现类包括 HashSet、TreeSet、LinkedHashSet 等。
- HashSet 基于哈希表实现,TreeSet 基于红黑树实现,LinkedHashSet 继承自 HashSet,但是内部使用 LinkedHashMap 来保存元素,保证了元素的插入顺序。
Map(映射):
- Map 是一种键值对的集合,每个键都映射到唯一的值。
- Map 中的键是唯一的,但值可以重复。
- 常见的实现类包括 HashMap、TreeMap、LinkedHashMap、Hashtable 等。
- HashMap 基于哈希表实现,TreeMap 基于红黑树实现,LinkedHashMap 继承自 HashMap,但是内部使用双向链表维护元素的插入顺序,Hashtable 是线程安全的,但性能比 HashMap 差。
总结一下底层数据结构:
- List:ArrayList(数组)、LinkedList(双向链表)、Vector(数组)
- Set:HashSet(哈希表)、TreeSet(红黑树)、LinkedHashSet(哈希表+链表)
- Map:HashMap(哈希表)、TreeMap(红黑树)、LinkedHashMap(哈希表+链表)、Hashtable(哈希表)
2.有哪些集合是线程不安全的,怎么解决?
在 Java 中,许多集合类都不是线程安全的。这些线程不安全的集合包括:
- ArrayList
- LinkedList
- HashSet
- HashMap
- TreeSet
- TreeMap
- LinkedHashSet
- LinkedHashMap
- 使用线程安全的集合类:Java 提供了对应的线程安全集合类,如 Vector、Hashtable、ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet 等。这些集合类在并发访问时能够保证线程安全。
- 使用同步集合包装器:通过 Collections.synchronizedXXX() 方法可以将线程不安全的集合转换为线程安全的集合。例如
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
Set<String> synchronizedSet = Collections.synchronizedSet(new HashSet<>());
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
- 手动同步:在使用线程不安全的集合时,通过手动在操作集合时添加同步块或使用同步方法来保证线程安全。例如:
List<String> list = new ArrayList<>();
synchronized (list) {
// 对 list 进行操作
}
- 使用并发集合类:Java 并发包提供了一系列高效的并发集合类,如 ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet 等。这些集合类针对特定的并发场景进行了优化,能够在高并发情况下提供较好的性能。
3.比较HashSet、LinkedHashSet 和 TreeSet 三者的异同?
HashSet、LinkedHashSet 和 TreeSet 都是 Java 中的集合实现,它们在存储和操作元素时有所不同。
HashSet:
- HashSet 是基于哈希表实现的集合,不保证元素的顺序,元素存储的顺序可能是不确定的。
- HashSet 允许存储 null 元素。
- HashSet 提供了常数时间复杂度的添加、删除、包含等操作。
- 适用于需要快速查找元素,不关心元素的顺序的场景。
LinkedHashSet:
- LinkedHashSet 继承自 HashSet,内部使用 LinkedHashMap 来维护元素的插入顺序。
- LinkedHashSet 保留了元素插入的顺序,因此遍历时元素的顺序是按照插入顺序排列的。
- LinkedHashSet 允许存储 null 元素。
- 添加、删除、包含等操作的时间复杂度与 HashSet 相同,但在遍历时会略慢一些。
TreeSet:
- TreeSet 是基于红黑树(自平衡二叉查找树)实现的有序集合,元素根据自然顺序或者比较器顺序进行排序。
- TreeSet 保证了元素的有序性,因此遍历时元素是有序的。
- TreeSet 不允许存储 null 元素。
- 添加、删除、包含等操作的时间复杂度与树的高度相关,通常情况下是对数时间复杂度,效率较高。
异同点总结:
- 相同点:都实现了 Set 接口,都不允许存储重复元素。
- 不同点:在存储顺序和底层实现上有所不同。HashSet 是基于哈希表实现的无序集合;LinkedHashSet 是基于哈希表和链表实现的有序集合;TreeSet 是基于红黑树实现的有序集合。
4.HashMap 和 HashTable 的区别? HashMap 和 HashSet 的区别? HashMap 和 TreeMap 的区别?
HashMap 和 HashTable 的区别:
- **线程安全性:**HashMap 是非线程安全的,而 HashTable 是线程安全的。
- **Null 键和值:**HashMap 允许一个键为 null,但只允许一个值为 null;而 HashTable 不允许键或值为 null。
- **迭代器:**HashMap 的迭代器是 fail-fast 的,而 HashTable 的迭代器是 fail-safe 的。
- **性能:**HashMap 的性能通常比 HashTable 要好,因为 HashMap 不进行同步,可以更快地执行。
- **扩容机制:**HashMap 的扩容机制是在数组容量超过一定阈值时扩容,而 HashTable 在达到装载因子时会自动进行扩容操作。
HashMap 和 HashSet 的区别:
- **存储结构:**HashMap 是键值对的集合,而 HashSet 是唯一值的集合。
- **底层实现:**HashMap 基于哈希表实现,HashSet 则是基于 HashMap 实现的,HashSet 中的元素实际上存储为 HashMap 中的键,而值为一个固定的 Object 对象。
- **元素重复:**HashMap 中键可以重复,但值不能重复;而 HashSet 中的元素都是唯一的。
HashMap 和 TreeMap 的区别:
- **存储结构:**HashMap 是无序的键值对集合,而 TreeMap 是有序的键值对集合,按照键的自然顺序或者比较器的顺序排序。
- **性能:**HashMap 的插入、删除、查找等操作的时间复杂度是 O(1),而 TreeMap 的时间复杂度是 O(log n)。
- **存储方式:**HashMap 是基于哈希表实现的,而 TreeMap 是基于红黑树实现的。
- **有序性:**HashMap 中的元素是无序的,而 TreeMap 中的元素是有序的。
5.HashMap 的底层实现?
HashMap 的底层实现是基于哈希表(Hash Table)的。在 Java 中,HashMap 是由数组和链表(或红黑树)组成的数据结构,用于存储键值对。下面是 HashMap 的基本结构和实现原理:
数组:
- HashMap 内部维护了一个数组,称为哈希桶数组(bucket array),用于存储实际的键值对元素。
- 数组的长度称为容量(capacity),通常会初始化为一个较大的值,并且是 2 的幂次方,如 16、32、64 等。
- 数组中的每个元素称为桶(bucket),每个桶可以存储一个链表或红黑树,用于解决哈希冲突。
哈希函数:
- 当添加一个键值对时,首先会对键进行哈希计算,得到键的哈希码。
- HashMap 使用一个哈希函数将键的哈希码映射到数组的某个位置,从而确定存储位置。
- 哈希函数通常会对哈希码进行一些处理,如取模运算(hash & (length - 1)),来确保键的分布均匀。
链表或红黑树:
- 如果发生哈希冲突(即多个键映射到了数组的同一个位置),则会将冲突的键值对存储在同一个桶中。
- 初始时,通常使用链表来存储冲突的键值对,如果同一个桶中的链表长度超过了阈值(JDK8 中默认为 8),则将链表转换为红黑树,以提高查找效率。
扩容和重新哈希:
- 当添加元素导致数组中的桶被占满时,HashMap 将会进行扩容操作。
- 扩容操作会创建一个新的更大的数组,然后将所有元素重新哈希到新数组中。
- 扩容操作的过程比较耗时,但由于哈希表的性质,扩容操作的平均时间复杂度是 O(1)。
哈希表的性能:
- HashMap 的添加、删除、查找等操作的时间复杂度是 O(1),即平均情况下是常数时间复杂度。
- 但在最坏情况下,即所有键映射到了同一个桶,哈希表的性能会退化为 O(n),因此要尽量避免哈希冲突的发生。
6.HashMap 的长度为什么是 2 的幂次方?
HashMap 的长度选择为 2 的幂次方是为了在计算哈希值映射到数组位置时采用位运算,这样可以提高效率。具体来说,长度为 2 的幂次方有以下几个优势:
- **利用位运算:**当长度为 2 的幂次方时,计算哈希值映射到数组位置可以用位运算来代替取模运算,如 (hash & (length - 1))。而位运算相比取模运算的性能更高,因此可以提高速度。
- **均匀分布:**哈希值的低几位可以保留原始哈希值的信息,而高位被舍弃,因此,对于不同的哈希值,它们在低几位的概率分布是均匀的。这样可以减少哈希冲突的发生,提高散列均匀性。
- **更高的性能:**HashMap 在插入、删除和查找操作时,需要计算元素的哈希值并根据长度计算存储位置。采用 2 的幂次方作为长度,可以通过位运算实现快速的哈希值映射,从而提高了性能。
- **扩容时的处理:**HashMap 在扩容时会将容量扩大到原来的两倍,而容量必须是 2 的幂次方。这样可以保证在扩容时不需要重新计算哈希值的位置,只需将原来位置的元素分配到新数组中即可,提高了扩容时的效率。
因此,选择长度为 2 的幂次方作为 HashMap 的长度是为了利用位运算的高效性和保证均匀分布,从而提高性能和效率。
7.ConcurrentHashMap 和 HashTable 的区别?
线程安全性:
- ConcurrentHashMap 使用了分段锁(Segment)的技术,将整个哈希表分为多个段,每个段独立加锁,不同的线程可以同时访问不同的段,从而提高了并发性能。
- HashTable 使用一个全局锁来保证线程安全,即在进行任何修改操作时都需要获得这个全局锁,因此在高并发情况下性能较差。
容量调整:
- ConcurrentHashMap 在容量不够时可以对其进行动态调整,而不需要锁定整个表,只需锁定一个段。这种分段锁的机制使得 ConcurrentHashMap 在并发环境下扩容的效率更高。
- HashTable 在进行扩容时需要锁定整个表,这意味着其他线程在这段时间内无法对表进行操作,性能较差。
迭代器:
- ConcurrentHashMap 的迭代器是弱一致性的(weakly consistent),允许在迭代过程中进行修改操作,但不保证能够看到修改的结果。这种设计可以提高并发性能,但迭代器的行为可能是不确定的。
- HashTable 的迭代器是强一致性的(fail-fast),在迭代过程中如果有其他线程对表进行了修改,则会抛出 ConcurrentModificationException 异常。
Null 值:
- ConcurrentHashMap 允许键和值为 null。
- HashTable 不允许键或值为 null,如果插入了 null 值或 null 键,会抛出 NullPointerException 异常。 性能:
- ConcurrentHashMap 的性能通常优于 HashTable,在高并发环境下表现更好。
- ConcurrentHashMap 的分段锁机制使得多个线程可以同时读取和写入不同的段,而 HashTable 在进行修改操作时需要锁定整个表,因此性能较差。
综上所述,ConcurrentHashMap 和 HashTable 在实现和性能上有很大的区别。ConcurrentHashMap 使用了分段锁机制来提高并发性能,并且具有更灵活的容量调整机制和更好的迭代器行为,因此在大多数情况下推荐使用 ConcurrentHashMap 来代替 HashTable。
8.ConcurrentHashMap 线程安全的具体实现方式底层具体实现
ConcurrentHashMap 在实现线程安全性时采用了一种分段锁(Segment Locking)的机制。这种机制将整个哈希表分为多个段(Segment),每个段拥有自己的锁,不同的线程可以同时访问不同的段,从而提高了并发性能。下面是 ConcurrentHashMap 的线程安全实现方式的详细介绍:
分段锁机制:
- ConcurrentHashMap 内部维护了一个 Segment 数组,Segment 是 ConcurrentHashMap 的内部静态类,它类似于一个小的 HashMap,包含一个哈希桶数组和一些用于控制并发的属性。
- 每个 Segment 都是一个独立的哈希表,它拥有自己的锁,不同的线程可以同时访问不同的 Segment,从而提高了并发性能。
- 当需要进行修改操作时,只需要锁定对应的 Segment,而不需要锁定整个 ConcurrentHashMap。
Segment 数组:
- ConcurrentHashMap 内部维护了一个 Segment 数组,数组的长度通常是 2 的幂次方,可以通过参数指定,默认为 16。
- 每个 Segment 都是一个独立的哈希表,它包含一个哈希桶数组和一些用于控制并发的属性,如锁、计数器等。
键值对的存储:
- 当需要插入或查找一个键值对时,首先计算键的哈希值,然后根据哈希值找到对应的 Segment,最后在该 Segment 中进行操作。
- 不同的键值对可能被存储在不同的 Segment 中,不同的线程可以同时操作不同的 Segment,提高了并发性能。
操作的并发控制:
- Segment 中的操作通常采用 synchronized 关键字进行并发控制,保证线程安全性。
- 在 JDK 8 中,对 ConcurrentHashMap 进行了优化,引入了 CAS(Compare and Swap)操作和 synchronized 关键字的混合使用,以提高并发性能。
扩容操作:
- ConcurrentHashMap 在需要扩容时,只需要扩容对应的 Segment,而不需要扩容整个 ConcurrentHashMap。
- 扩容操作会将 Segment 的大小扩大为原来的两倍,然后将原来的元素重新分配到新的 Segment 中。
综上所述,ConcurrentHashMap 通过分段锁机制来实现线程安全性,将整个哈希表分为多个段,每个段拥有自己的锁,从而实现了更细粒度的并发控制。这种机制提高了 ConcurrentHashMap 在高并发环境下的性能,并且能够动态调整容量,提高了效率。