余声-个人博客


  • 首页

  • 分类

  • 归档

  • 标签

为什么hashMap的容量扩容时一定是2的幂次

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌为什么hashMap的容量扩容时一定是2的幂次?

题目详细答案

在HashMap中,初始化设置长度时,容量自动转成 2 的幂次长度,这样设计有几个重要原因,主要是为了优化性能和简化计算。

高效计算索引

HashMap使用哈希值来确定键值对在哈希表中的位置。为了计算数组索引,HashMap使用按位与操作代替取模运算。具体来说,HashMap通过以下方式计算索引:

1
int index= (n - 1) & hash;

其中n是哈希表数组的长度。假设n是 2 的幂次,比如 16(2^4),则n - 1是 15(1111 二进制)。这样,(n - 1) & hash操作可以快速地对哈希值进行取模运算,而不需要使用性能较低的取模操作%。

例如,如果n是 16(2^4):

  • n - 1是 15(1111 二进制)
  • 按位与操作(n - 1) & hash只保留哈希值的低 4 位,这相当于对 16 取模。

这种方式不仅计算快速,而且代码简洁。

减少哈希冲突

在哈希表中,哈希冲突是一个主要问题。哈希冲突发生时,不同的键计算出的索引相同,导致它们被存储在同一个桶中。通过将容量设置为 2 的幂次,哈希表能够更均匀地分布哈希值,减少冲突。

具体来说,当容量是 2 的幂次时,哈希值的低位和高位都能均匀地影响最终索引。这是因为扰动函数hash = h ^ (h >>> 16)将高位和低位混合在一起,使得哈希值的分布更均匀。

假设我们有一个HashMap,其容量为 10(不是 2 的幂次),并且我们有一组键,它们的哈希值分别为:

  • 键 A 的哈希值:35
  • 键 B 的哈希值:45
  • 键 C 的哈希值:55

(1)使用非 2 的幂次容量

如果容量为 10,我们计算索引的方法是取模运算:

  • 键 A 的索引:35 % 10 = 5
  • 键 B 的索引:45 % 10 = 5
  • 键 C 的索引:55 % 10 = 5

可以看到,这些不同的键在取模运算后都映射到同一个索引 5,导致了哈希冲突。

(2)使用 2 的幂次容量

现在假设我们将容量设置为 16(2^4),并使用按位与操作来计算索引:

  • 键 A 的索引:35 & (16 - 1) = 35 & 15 = 3
  • 键 B 的索引:45 & (16 - 1) = 45 & 15 = 13
  • 键 C 的索引:55 & (16 - 1) = 55 & 15 = 7

在这种情况下,这些键映射到不同的索引(3、13 和 7),没有发生哈希冲突。

简化扩容

HashMap在需要扩容时,通常会将容量加倍。如果容量总是 2 的幂次,那么加倍后的容量仍然是 2 的幂次,这样可以简化扩容过程中的计算和重新哈希操作。

内存对齐和效率

计算机内存分配通常更高效地处理 2 的幂次大小的内存块。使用 2 的幂次长度可以更好地利用内存对齐,提高内存访问效率。

实现细节

当你初始化HashMap时,指定的初始容量会被调整为大于或等于该值的最小的 2 的幂次。例如,如果你指定的初始容量是 10,HashMap会将其调整为 16(2^4)。

具体实现如下:

1
2
3
4
5
6
7
8
9
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

这个方法通过一系列的位移和按位或操作,将任意整数调整为大于或等于它的最小 2 的幂次。但是还有一种特殊情况套用以上公式不行,这些数字就是2的幂自身。如果数字4 套用公式的话。得到的会是 8 ,为了解决这个问题,JDK的工程师把所有用户传进来的数在进行计算之前先-1。

/yfxseowdd8g5hm0e>

为什么hashmap多线程会进入死循环?

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌为什么hashmap多线程会进入死循环?

题目详细答案

HashMap在多线程环境中可能会进入死循环,主要是由于其非线程安全的设计导致的。

并发修改导致的链表环

在HashMap中,当发生哈希冲突时,使用链地址法(链表)来存储冲突的键值对。如果多个线程同时对HashMap进行修改(例如插入或删除操作),可能会导致链表结构被破坏,形成环形链表。这种情况下,当遍历链表时,会陷入死循环。

原因分析

当两个或多个线程同时修改HashMap,例如在同一个桶中插入元素,可能会导致链表的指针被错误地更新。例如,一个线程正在将一个新的节点插入链表中,而另一个线程正在重新排列链表的顺序。这种竞争条件可能导致链表中出现环形结构。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.HashMap;
import java.util.Map;

public class HashMapInfiniteLoop {
public static void main(String[] args) {
final Map<Integer, Integer> map = new HashMap<>();

// 创建两个线程同时对 HashMap 进行插入操作
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
map.put(i, i);
}
});

Thread t2 = new Thread(() -> {
for (int i = 10000; i < 20000; i++) {
map.put(i, i);
}
});

t1.start();
t2.start();

try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

// 遍历 HashMap,可能会陷入死循环
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
}
}

在上述代码中,两个线程同时对HashMap进行插入操作,可能会导致链表结构被破坏,形成环形链表,从而在遍历时陷入死循环。

扩容导致的并发问题

HashMap在容量达到一定阈值时会进行扩容(rehash),即重新分配桶数组,并重新哈希所有键值对。如果在扩容过程中,有其他线程同时进行插入操作,可能会导致重新哈希过程中的数据不一致,进而引发死循环。

原因分析

扩容过程中,HashMap会创建一个新的、更大的桶数组,并将所有旧的键值对重新哈希并放入新的桶中。如果在这个过程中有其他线程插入新的键值对,可能会导致旧桶和新桶的数据结构不一致,进而引起死循环。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.HashMap;
import java.util.Map;

public class HashMapResizeInfiniteLoop {
public static void main(String[] args) {
final Map<Integer, Integer> map = new HashMap<>(2);

// 创建两个线程同时对 HashMap 进行插入操作
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
map.put(i, i);
}
});

Thread t2 = new Thread(() -> {
for (int i = 10000; i < 20000; i++) {
map.put(i, i);
}
});

t1.start();
t2.start();

try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

// 遍历 HashMap,可能会陷入死循环
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
}
}

在上述代码中,HashMap初始容量设置为 2,两个线程同时插入大量元素,可能会导致扩容过程中数据不一致,从而引发死循环。

解决方案

使用线程安全的数据结构

在多线程环境中,使用ConcurrentHashMap代替HashMap。ConcurrentHashMap通过分段锁机制来保证线程安全,并发性能更好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

public class ConcurrentHashMapExample {
public static void main(String[] args) {
final Map<Integer, Integer> map = new ConcurrentHashMap<>();

Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
map.put(i, i);
}
});

Thread t2 = new Thread(() -> {
for (int i = 10000; i < 20000; i++) {
map.put(i, i);
}
});

t1.start();
t2.start();

try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
}
}

外部同步

如果必须使用HashMap,可以在外部进行同步,确保同时只有一个线程对HashMap进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.util.HashMap;
import java.util.Map;

public class SynchronizedHashMapExample {
public static void main(String[] args) {
final Map<Integer, Integer> map = new HashMap<>();

Thread t1 = new Thread(() -> {
synchronized (map) {
for (int i = 0; i < 10000; i++) {
map.put(i, i);
}
}
});

Thread t2 = new Thread(() -> {
synchronized (map) {
for (int i = 10000; i < 20000; i++) {
map.put(i, i);
}
}
});

t1.start();
t2.start();

try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (map) {
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
}
}
}

通过使用ConcurrentHashMap或外部同步,可以避免HashMap在多线程环境中出现死循环的问题。

/dhuaw13mlcgfpa9k>

为什么要使用扰动函数?

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌为什么要使用扰动函数?

题目详细答案

扰动函数的目的是为了提高哈希码的质量,使其在哈希表中更均匀地分布。具体来说:

减少哈希冲突:通过将高位和低位混合,扰动函数减少了哈希码的模式性,降低了哈希冲突的概率。

均匀分布:扰动后的哈希码更加均匀地分布在哈希表的桶中,从而提高了哈希表的性能。

示例 Demo

假设我们有一个键对象,其hashCode()返回值为123456。我们可以通过哈希函数计算其哈希值:

  1. 调用hashCode()方法:
1
int h = 123456;
  1. 扰动函数计算:
1
int hash = h ^ (h >>> 16);
  1. 具体计算步骤:
    • h >>> 16 = 123456 >>> 16 = 1(右移 16 位)
    • hash = 123456 ^ 1 = 123457(异或运算)

最终,哈希值为123457。

/ntmpkg1lprz393fp>

什么是HashTable

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌什么是HashTable?

题目详细答案

Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。

与其他集合类的比较

Hashtable vs. HashMap:

Hashtable是线程安全的,而HashMap不是。

Hashtable不允许键或值为null,而HashMap允许一个null键和多个null值。

在现代 Java 编程中,HashMap更常用,因为它在大多数情况下性能更好,并且可以通过外部同步来实现线程安全。

Hashtable vs. ConcurrentHashMap:

ConcurrentHashMap是 Java 5 引入的一种改进的哈希表实现,专为高并发环境设计。

ConcurrentHashMap提供了更细粒度的锁机制,允许更高的并发性和更好的性能。

总的来说,Hashtable是一种较早的哈希表实现,适用于需要线程安全的简单场景。然而,在现代 Java 开发中,通常推荐使用ConcurrentHashMap或通过外部同步来使用HashMap,以获得更好的性能和灵活性。

/bgy4pz59q8ffzxwo>

什么是LinkedHashMap

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌什么是LinkedHashMap?

题目详细答案

LinkedHashMap是 Java 集合框架中的一个类,它继承自HashMap,并且具有哈希表和链表的双重特性。LinkedHashMap通过维护一个双向链表来记录键值对的插入顺序或访问顺序,从而提供了一种有序的映射。

基本特点

  1. 有序性:LinkedHashMap保证了键值对的顺序,可以是插入顺序(默认)或访问顺序(可选)。
  2. 哈希表和链表结合:LinkedHashMap通过哈希表实现快速的键值对查找,同时通过双向链表维护顺序。
  3. 允许null键和值:LinkedHashMap允许一个null键和多个null值。
  4. 线程不安全:LinkedHashMap不是线程安全的,如果需要在多线程环境中使用,需要通过外部同步机制来保证线程安全。

与其他集合类的比较

LinkedHashMap vs. HashMap:

LinkedHashMap保证键值对的顺序(插入顺序或访问顺序),而HashMap不保证顺序。

LinkedHashMap通过维护链表来记录顺序,因此在插入和删除操作上可能略慢于HashMap。

LinkedHashMap vs. TreeMap:

LinkedHashMap保证插入顺序或访问顺序,而TreeMap保证键的自然顺序或比较器的顺序。

LinkedHashMap基于哈希表实现,操作的平均时间复杂度为 O(1),而TreeMap基于红黑树实现,操作的时间复杂度为 O(log n)。

适用场景

LinkedHashMap适用于需要保持键值对的插入顺序或访问顺序的场景:

实现 LRU(最近最少使用)缓存。

需要按插入顺序遍历键值对。

需要在保持顺序的同时快速查找键值对。

/ttdsl1h3wbwte5u4>

什么是TreeMap

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌什么是TreeMap?

题目详细答案

基本特点

  1. 有序性:TreeMap保证了键的自然顺序(通过Comparable接口)或通过提供的比较器(Comparator)的顺序。
  2. 红黑树:TreeMap内部使用红黑树数据结构来存储键值对,保证了插入、删除、查找等操作的时间复杂度为 O(log n)。
  3. 不允许null键:TreeMap不允许键为null,但允许值为null。
  4. 线程不安全:TreeMap不是线程安全的,如果需要在多线程环境中使用,需要通过外部同步机制来保证线程安全。

与其他集合类的比较

TreeMap vs. HashMap:

TreeMap保证键的有序性,而HashMap不保证顺序。

TreeMap基于红黑树实现,操作的时间复杂度为 O(log n),而HashMap基于哈希表实现,操作的平均时间复杂度为 O(1)。

TreeMap不允许null键,而HashMap允许一个null键。

TreeMap vs. LinkedHashMap:

TreeMap保证键的自然顺序或比较器的顺序,而LinkedHashMap保证插入顺序或访问顺序。

TreeMap的操作时间复杂度为 O(log n),而LinkedHashMap的操作时间复杂度为 O(1)。

适用场景

TreeMap适用于需要按键排序存储键值对的场景,例如:

实现基于范围的查询。

需要按顺序遍历键值对。

需要快速查找最小或最大键值对。

/angmwmt1ka8unn4o>

fail-fast机制

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌什么是fail-fast机制?

题目详细答案

在Java集合框架中,fail-fast是一种机制,用于检测在遍历集合时的结构性修改,并立即抛出异常以防止不一致状态。fail-fast迭代器在检测到集合在迭代过程中被修改后,会抛出ConcurrentModificationException异常。

工作原理

fail-fast迭代器通过在遍历集合时维护一个修改计数器(modification count)来工作。每当集合结构发生变化(如添加或删除元素)时,这个计数器就会增加。当创建迭代器时,它会保存当前的修改计数器值。在每次调用next()方法时,迭代器会检查当前的修改计数器值是否与保存的值一致。如果不一致,说明集合在迭代过程中被修改了,迭代器会立即抛出ConcurrentModificationException。

代码 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.*;

public class FailFastExample {
public static void main(String[] args) {
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);
// 在迭代过程中修改集合
if (element.equals("B")) {
list.add("D"); // 这将引发 ConcurrentModificationException
}
}
}
}

在上面的代码中,当迭代器遍历到元素 “B” 时,集合被修改(添加了新元素 “D”),因此迭代器将抛出ConcurrentModificationException。

注意事项

快速失败并不保证:fail-fast机制并不能保证在所有情况下都能检测到并发修改。它是尽力而为的检测机制,不能依赖于它来实现并发安全。如果需要并发安全的集合,可以使用java.util.concurrent包中的并发集合类。

避免并发修改:在遍历集合时,避免在外部修改集合。可以使用迭代器的remove方法来安全地移除元素。

使用remove方法

为了避免ConcurrentModificationException,可以使用迭代器的remove方法来移除元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.*;

public class SafeRemovalExample {
public static void main(String[] args) {
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);
// 使用迭代器的 remove 方法安全地移除元素
if (element.equals("B")) {
iterator.remove();
}
}

System.out.println("After removal: " + list);
}
}

在这个示例中,使用iterator.remove()方法安全地移除了元素 “B”。

fail-safe机制

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌什么是fail-safe机制?

题目详细答案

fail-safe机制是与fail-fast机制相对的一种并发处理机制。在 Java 集合框架中,fail-safe迭代器在检测到集合在遍历过程中被修改时,不会抛出异常,而是允许这种修改继续进行。fail-safe迭代器通常是通过在遍历时使用集合的副本来实现的,这样即使原集合被修改,迭代器也不会受到影响。

工作原理

fail-safe迭代器在遍历集合时,实际上是遍历集合的一个副本。因此,任何对原集合的修改都不会影响到迭代器正在遍历的副本。这种机制保证了遍历操作的安全性,但也意味着迭代器不能反映集合的实时变化。

代码 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Iterator;
public class FailSafeExample {
publicstaticvoidmain(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
Stringelement= iterator.next();
System.out.println(element);
// 在迭代过程中修改集合
if (element.equals("B")) {
list.add("D"); // 不会引发 ConcurrentModificationException
}
}

System.out.println("After modification: " + list);
}
}

在上面的代码中,使用CopyOnWriteArrayList作为集合。CopyOnWriteArrayList是一个典型的fail-safe集合类,它在每次修改时都会创建集合的一个副本,因此迭代器不会检测到并发修改,不会抛出ConcurrentModificationException。

主要特点

  1. 不抛异常:fail-safe迭代器在检测到集合被修改时,不会抛出ConcurrentModificationException异常。
  2. 副本遍历:fail-safe迭代器遍历的是集合的一个副本,而不是原集合。这意味着对原集合的修改不会影响迭代器的遍历。
  3. 线程安全:fail-safe集合类(如CopyOnWriteArrayList、ConcurrentHashMap等)通常是线程安全的,适用于并发环境。

常见的fail-safe集合类

CopyOnWriteArrayList,ConcurrentHashMap,ConcurrentLinkedQueue,ConcurrentSkipListMap,ConcurrentSkipListSet

注意事项

  1. 性能开销:由于fail-safe机制通常需要创建集合的副本,因此在修改频繁的场景下,性能开销较大。适用于读多写少的场景。
  2. 一致性问题:由于迭代器遍历的是集合的副本,因此它不能反映集合的实时变化。如果需要实时一致性,fail-safe机制可能不适用。

介绍一下HashMap?

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌介绍一下HashMap?

题目详细答案

HashMap主要是用于存储键值对。它是基于哈希表实现的,提供了快速的插入、删除和查找操作。从安全角度,HashMap不是线程安全的。如果多个线程同时访问一个HashMap并且至少有一个线程修改了它,则必须手动同步。

HashMap允许一个 null 键和多个 null 值。

HashMap不保证映射的顺序,特别是它不保证顺序会随着时间的推移保持不变。

HashMap提供了 O(1) 时间复杂度的基本操作(如 get 和 put),前提是哈希函数的分布良好且冲突较少。

HashMap主要方法

**put(K key, V value)**:将指定的值与该映射中的指定键关联。如果映射以前包含一个该键的映射,则旧值将被替换。

**get(Object key)**:返回指定键所映射的值;如果此映射不包含该键的映射,则返回 null。

**remove(Object key)**:如果存在一个键的映射,则将其从映射中移除。

**containsKey(Object key)**:如果此映射包含指定键的映射,则返回 true。

**containsValue(Object value)**:如果此映射将一个或多个键映射到指定值,则返回 true。

**size()**:返回此映射中的键值映射关系的数量。

**isEmpty()**:如果此映射不包含键值映射关系,则返回 true。

**clear()**:从此映射中移除所有键值映射关系。

代码 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.HashMap;
import java.util.Map;

public class HashMapExample {
public static void main(String[] args) {
// 创建一个 HashMap 实例
Map<String, Integer> map = new HashMap<>();

// 添加键值对
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);

// 访问元素
System.out.println("Value for key 'apple': " + map.get("apple"));

// 检查是否包含某个键或值
System.out.println("Contains key 'banana': " + map.containsKey("banana"));
System.out.println("Contains value 3: " + map.containsValue(3));

// 移除元素
map.remove("orange");

// 遍历 HashMap
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}

// 获取大小
System.out.println("Size of map: " + map.size());

// 清空 HashMap
map.clear();
System.out.println("Is map empty: " + map.isEmpty());
}
}

内部工作原理

HashMap使用哈希表来存储数据。哈希表是基于数组和链表的组合结构。

  1. 哈希函数:HashMap使用键的hashCode()方法来计算哈希值,然后将哈希值映射到数组的索引位置。
  2. 数组和链表:HashMap使用一个数组来存储链表或树结构(Java 8 及以后)。每个数组位置被称为一个“桶”,每个桶存储链表或树。
  3. 冲突处理:当两个键的哈希值相同时,它们会被存储在同一个桶中,形成一个链表(或树)。这种情况称为哈希冲突。
  4. 再哈希:当HashMap中的元素数量超过容量的负载因子(默认 0.75)时,HashMap会进行再哈希,将所有元素重新分配到一个更大的数组中。

注意事项

初始容量和负载因子:可以通过构造函数设置HashMap的初始容量和负载因子,以优化性能。初始容量越大,减少再哈希的次数;负载因子越小,减少冲突的概率,但会增加空间开销。

哈希函数的质量:哈希函数的质量直接影响HashMap的性能。理想的哈希函数应尽可能均匀地分布键。

线程安全性

如前所述,HashMap不是线程安全的。如果需要线程安全的映射,可以使用Collections.synchronizedMap来包装HashMap,或者使用ConcurrentHashMap,后者在高并发环境下性能更好。

1
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(newHashMap<>());

或者使用ConcurrentHashMap:

1
Map<String, Integer> concurrentMap = newConcurrentHashMap<>();

/iwe6pgyi65omsy8l>

常见的list实现类

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌介绍一下常见的list实现类?

ArrayList 是最常用的 List 实现类,线程不安全,内部是通过数组实现的,继承了AbstractList,实现了List。它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。排列有序,可重复,容量不够的时候,新容量的计算公式为:

newCapacity = oldCapacity + (oldCapacity >> 1),这实际上是将原容量增加50%(即乘以1.5)。

ArrayList实现了RandomAccess接口,即提供了随机访问功能。RandomAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。

ArrayList实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。

LinkedList(链表)

LinkedList 是用链表结构存储数据的,线程不安全。很适合数据的动态插入和删除,随机访问和遍历速度比较慢,增删快。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。底层使用双向链表数据结构。

LinkedList是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。

Vector(数组实现、线程同步)

Vector 与 ArrayList 一样,也是通过数组实现的,Vector和ArrayList用法上几乎相同,但Vector比较古老,一般不用。Vector是线程同步的,效率低。即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。默认扩展一倍容量。

/op8ey0ndh6st3bgz>

如何确保函数不能修改集合

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌如何确保函数不能修改集合?

题目详细答案

使用Collections.unmodifiableCollection方法

Java提供了Collections.unmodifiableCollection方法,可以将一个集合包装成一个不可修改的视图。对这个视图的修改操作将会抛出UnsupportedOperationException。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.*;

public class UnmodifiableCollectionExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Collection<String> unmodifiableList = Collections.unmodifiableCollection(list);

// 传递不可修改的集合给函数
printCollection(unmodifiableList);

// 尝试修改集合将抛出 UnsupportedOperationException
// unmodifiableList.add("D"); // 这行代码会抛出异常
}

public static void printCollection(Collection<String> collection) {
for (String item : collection) {
System.out.println(item);
}
}
}

使用Collections.unmodifiable

对于特定类型的集合,如List、Set和Map,Java 提供了相应的不可修改视图方法:

Collections.unmodifiableList

Collections.unmodifiableSet

Collections.unmodifiableMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.util.*;

public class UnmodifiableSpecificCollectionsExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
List<String> unmodifiableList = Collections.unmodifiableList(list);

Set<String> set = new HashSet<>(Arrays.asList("X", "Y", "Z"));
Set<String> unmodifiableSet = Collections.unmodifiableSet(set);

Map<String, Integer> map = new HashMap<>();
map.put("One", 1);
map.put("Two", 2);
Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);

// 传递不可修改的集合给函数
printList(unmodifiableList);
printSet(unmodifiableSet);
printMap(unmodifiableMap);

// 尝试修改集合将抛出 UnsupportedOperationException
// unmodifiableList.add("D"); // 这行代码会抛出异常
// unmodifiableSet.add("W"); // 这行代码会抛出异常
// unmodifiableMap.put("Three", 3); // 这行代码会抛出异常
}

public static void printList(List<String> list) {
for (String item : list) {
System.out.println(item);
}
}

public static void printSet(Set<String> set) {
for (String item : set) {
System.out.println(item);
}
}

public static void printMap(Map<String, Integer> map) {
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}

使用Collections.unmodifiableCollection递归包装嵌套集合

如果集合中包含嵌套集合(例如一个List中包含Set),你需要递归地将所有嵌套集合也包装成不可修改的视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.*;

public class UnmodifiableNestedCollectionsExample {
public static void main(String[] args) {
List<Set<String>> listOfSets = new ArrayList<>();
listOfSets.add(new HashSet<>(Arrays.asList("A", "B", "C")));
listOfSets.add(new HashSet<>(Arrays.asList("X", "Y", "Z")));

List<Set<String>> unmodifiableListOfSets = new ArrayList<>();
for (Set<String> set : listOfSets) {
unmodifiableListOfSets.add(Collections.unmodifiableSet(set));
}
Collection<List<Set<String>>> unmodifiableCollection = Collections.unmodifiableCollection(Collections.singletonList(unmodifiableListOfSets));

// 传递不可修改的集合给函数
printNestedCollection(unmodifiableCollection);

// 尝试修改集合将抛出 UnsupportedOperationException
// unmodifiableListOfSets.get(0).add("D"); // 这行代码会抛出异常
}

public static void printNestedCollection(Collection<List<Set<String>>> collection) {
for (List<Set<String>> list : collection) {
for (Set<String> set : list) {
for (String item : set) {
System.out.println(item);
}
}
}
}
}

通过以上可以确保传递给函数的集合不会被修改,从而保证集合的不可变性。

如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

题目详细答案

当HashMap的大小超过了负载因子(load factor)定义的容量时,HashMap会进行扩容(resize)。

扩容的具体步骤

假设当前HashMap的容量是N,负载因子是L,当插入的键值对数量超过N * L时,HashMap会进行扩容。具体步骤如下:

  1. 计算新的容量: 新的容量通常是当前容量的两倍。假设当前容量是N,新的容量是2 * N。
  2. 创建新的桶数组: 根据新的容量创建一个新的桶数组。
  3. 重新哈希所有的键值对: 对旧桶数组中的所有键值对重新计算哈希值,并将它们放入新的桶数组中。这一步是必要的,因为哈希值是基于数组长度计算的,数组长度改变后,哈希值也需要重新计算。

代码 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.HashMap;
import java.util.Map;

public class HashMapResizeExample {
public static void main(String[] args) {
// 创建一个初始容量为4,负载因子为0.75的HashMap
Map<Integer, String> map = new HashMap<>(4, 0.75f);

// 插入一些键值对
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
// 触发扩容
map.put(4, "four");

// 打印扩容后的HashMap
System.out.println(map);
}
}

在上述代码中,当插入第四个键值对时,HashMap的大小超过了4 * 0.75 = 3,因此会触发扩容,容量从 4 增加到 8。

扩容的具体实现

在 Java 的HashMap实现中,扩容的具体代码在resize方法中。以下是resize方法的简化版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void resize() {
// 计算新的容量
int newCapacity = oldCapacity * 2;

// 创建新的桶数组
Node<K,V>[] newTable = (Node<K,V>[])new Node[newCapacity];

// 重新哈希所有的键值对
for (Node<K,V> node : oldTable) {
while (node != null) {
Node<K,V> next = node.next;
int hash = node.hash % newCapacity;
node.next = newTable[hash];
newTable[hash] = node;
node = next;
}
}

// 替换旧的桶数组
table = newTable;
}

注意事项

性能影响:扩容是一个相对昂贵的操作,因为它需要重新哈希并移动所有现有的键值对。因此,在设计HashMap时,应尽量选择合适的初始容量和负载因子,以减少扩容次数。

线程安全:默认的HashMap是非线程安全的。如果在多线程环境中使用HashMap,需要使用Collections.synchronizedMap或使用ConcurrentHashMap。

内存消耗:扩容会临时占用更多的内存,因为需要同时维护旧的和新的桶数组。因此,在内存受限的环境中需要谨慎使用。

拉链法导致的链表过深问题为什么用红黑树

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌拉链法导致的链表过深问题为什么用红黑树?

题目详细答案

在JDK 8中,HashMap采用红黑树来解决拉链法导致的链表过深问题,主要是为了提高在高冲突情况下的性能。

拉链法中的链表过深问题

在传统的拉链法中,当多个键的哈希值冲突时,这些键会被存储在同一个桶(bucket)中,形成一个链表。如果链表过长,查找、插入和删除操作的时间复杂度会退化为O(n),其中n是链表中的元素个数。这种情况下,HashMap的性能会显著下降。

红黑树的优势

红黑树是一种自平衡的二叉搜索树,具有以下特性:

  1. 自平衡:红黑树通过颜色属性(红色和黑色)和一系列的旋转操作,保证树的高度近似平衡。其高度不会超过2 * log(n),其中n是树中的节点数。
  2. 高效的查找、插入和删除:红黑树的查找、插入和删除操作的时间复杂度为O(log n),远优于链表在最坏情况下的O(n)。

为什么选择红黑树

  1. 性能优化:在链表长度较短时,链表操作的性能是可以接受的。然而,当链表长度超过一定阈值(JDK 8中为8)时,链表操作的性能会显著下降。此时,将链表转换为红黑树,可以将操作的时间复杂度从O(n)降低到O(log n),显著提高性能。
  2. 平衡性:红黑树通过自平衡机制,保证树的高度始终保持在一个较低的水平,避免了链表过长导致的性能问题。
  3. 空间效率:虽然红黑树比链表占用更多的空间(因为需要存储颜色和指针信息),但在链表长度较长时,性能的提升通常会超过空间开销的增加。

实现细节

在JDK 8的HashMap中,当链表长度超过阈值(8)时,会将链表转换为红黑树。

1
2
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);

treeifyBin方法将链表转换为红黑树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}

聊聊常见set类都有哪几种

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌聊聊常见set类都有哪几种?

题目详细答案

在Java中,Set是一种不包含重复元素的集合,它继承自Collection接口。用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。Java中常见的Set实现类主要有三个:HashSet、LinkedHashSet和TreeSet。

HashSet

HashSet是Set接口的一个实现类,它基于哈希表实现,具有快速的插入、删除和查找操作。

HashSet不保证元素的迭代顺序,允许null元素的存在,但只能有一个null元素,不是线程安全的,如果多个线程同时访问并修改HashSet,则需要外部同步。

当存储自定义对象时,需要重写对象的hashCode()和equals()方法,以确保对象的唯一性。

LinkedHashSet

LinkedHashSet是HashSet的子类,它基于哈希表和双向链表实现,继承与 HashSet、又基于 LinkedHashMap 来实现的。可以维护元素的插入顺序。相比于 HashSet 增加了顺序性。

其主要是将元素按照插入的顺序进行迭代,同时继承了HashSet的所有特性。因此当需要保持元素插入顺序时,可以选择使用LinkedHashSet。

TreeSet

TreeSet是Set接口的另一个实现类,它基于红黑树实现,可以对元素进行自然排序或自定义排序。

可以实现元素按照升序进行排序(自然排序或自定义排序)。不过它不允许null元素的存在。treeset 同样是线程不安全的。

当存储自定义对象时,如果想要进行排序,需要实现Comparable接口并重写compareTo()方法,或提供自定义的Comparator对象来进行排序。

适用场景

HashSet:适用于需要快速查找的场景,不保证元素的顺序。

LinkedHashSet:适用于需要保持元素插入顺序的场景。

TreeSet:适用于需要元素排序的场景。

说一下java8实现的concurrentHashMap?

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌说一下java8实现的concurrentHashMap?

题目详细答案

Java 8 对ConcurrentHashMap进行了重新设计,取消了分段锁的机制,改用更细粒度的锁和无锁操作来提高并发性能。

数据结构

Node:基本的链表节点,存储键值对和指向下一个节点的指针。

TreeNode:用于红黑树的节点,当链表长度超过一定阈值(默认是8)时,链表会转换为红黑树。

TreeBin:红黑树的容器,管理红黑树的操作。

ForwardingNode:在扩容过程中用于指示节点已经被移动。

主要操作

put 操作:通过 CAS 操作和细粒度的锁来实现高效的并发插入和更新。

get 操作:使用无锁的方式进行查找,性能更高。

扩容:通过逐步迁移节点和协作扩容机制,提高扩容效率。

细粒度的并发控制

Java 8 中的ConcurrentHashMap采用了更细粒度的并发控制,主要通过以下方式实现:

CAS 操作:使用 CAS 操作(Compare-And-Swap)进行无锁插入和更新,减少锁竞争。

synchronized 块:在必要时对单个桶(bin)进行加锁,而不是整个段,从而进一步提高并发性。

红黑树:当链表长度超过阈值时,转换为红黑树,降低查找时间复杂度,从 O(n) 降低到 O(log n)。

Java 8 相比 Java 7 的好处

更高的并发性:Java 7 使用段级别的锁,而 Java 8 使用更细粒度的锁和无锁操作,减少了锁竞争,提高了并发性。

更好的性能:Java 8 中的get操作是无锁的,性能更高。put操作使用 CAS 和细粒度的锁,提高了插入和更新的性能。

更高效的扩容:Java 8 通过逐步迁移节点和协作扩容机制,提高了扩容效率,减少了扩容过程中对性能的影响。

更高效的查找:当链表长度超过阈值时,转换为红黑树,降低了查找时间复杂度。

/kfgmeauv8z37pxgd>

重新调整HashMap大小存在什么问题吗?

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌重新调整HashMap大小存在什么问题吗?

题目详细答案

性能影响

时间复杂度:扩容是一个相对昂贵的操作,因为它需要重新计算所有现有键值对的哈希值,并将它们重新分配到新的桶数组中。这个过程的时间复杂度为 (O(n)),其中 (n) 是哈希表中元素的数量。因此,在扩容期间,可能会导致性能的短暂下降,尤其是在插入大量数据时。

阻塞操作:在单线程环境中,扩容会阻塞其他操作(如查找、插入、删除),直到扩容完成。在多线程环境中,如果没有适当的同步机制,扩容可能会导致数据不一致或其他并发问题。

内存使用

临时内存消耗:扩容期间,HashMap需要分配一个新的桶数组,同时保留旧的桶数组,直到重新哈希完成。这会导致临时的内存消耗增加。如果哈希表非常大,可能会导致内存不足的问题。

内存碎片:频繁的扩容和缩容可能导致内存碎片化,降低内存利用效率。

并发问题

线程安全:默认的HashMap不是线程安全的。在多线程环境中,如果一个线程在进行扩容操作,而另一个线程在进行插入或删除操作,可能会导致数据不一致或程序崩溃。为了解决这个问题,可以使用ConcurrentHashMap或在外部进行同步。

扩容期间的数据一致性:在扩容过程中,如果有其他线程在进行读写操作,可能会导致数据不一致。因此,在多线程环境中,必须确保扩容操作是原子的,或者使用并发安全的数据结构。

重新哈希的成本

哈希函数的复杂性:重新哈希所有键值对需要调用哈希函数。如果哈希函数较为复杂,重新哈希的成本也会增加。

哈希冲突的处理:在扩容过程中,哈希冲突的处理(如链地址法中的链表或红黑树)也会增加额外的开销。

应用层面的影响

实时性要求:在某些实时性要求较高的应用中,扩容操作可能导致短暂的性能下降,影响系统的响应时间。

数据一致性要求:在某些应用中,数据的一致性要求较高,扩容过程中可能会导致短暂的数据不一致,需要额外的机制来保证一致性。

解决方案和优化

  1. 预估初始容量:如果可以预估数据量,尽量在创建HashMap时设置合适的初始容量,减少扩容次数。
  2. 使用并发数据结构:在多线程环境中,使用ConcurrentHashMap代替HashMap,它采用了分段锁机制,减少了扩容带来的并发问题。
  3. 动态调整负载因子:根据应用需求,动态调整负载因子以适应数据量的变化。

/gifnny9n0u67fo68>

阻塞队列原理?

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌阻塞队列原理?

题目详细答案

阻塞队列是一种线程安全的队列,它在插入和删除操作上可以阻塞线程,以实现生产者-消费者模式等并发编程需求。阻塞队列的核心原理包括锁机制和条件变量。

基本原理

锁机制

阻塞队列使用锁(如ReentrantLock)来确保线程安全。锁保证了同一时间只有一个线程可以执行插入或删除操作,从而避免并发问题。

条件变量

阻塞队列使用条件变量(Condition)来管理线程的等待和通知。条件变量是与锁关联的,可以在特定条件下阻塞线程并在条件满足时唤醒线程。例如,notEmpty和notFull是常见的条件变量,分别用于表示队列是否为空和是否已满。

等待和通知机制:

当线程试图执行插入操作而队列已满时,它会在notFull条件变量上等待,直到队列中有空闲空间。

当线程试图执行删除操作而队列为空时,它会在notEmpty条件变量上等待,直到队列中有可用的元素。

当插入或删除操作成功后,相应的条件变量会被通知(唤醒),以便其他等待的线程可以继续执行。

具体实现 Demo

LinkedBlockingQueue是一个基于链表实现的可选有界阻塞队列。它的基本原理如下:

内部结构:使用一个链表来存储元素。使用两个锁:takeLock和putLock,分别用于控制删除和插入操作。使用两个条件变量:notEmpty和notFull,分别用于表示队列是否为空和是否已满。

插入操作(put):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity) {
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0) {
signalNotEmpty();
}
}

删除操作(take):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1) {
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity) {
signalNotFull();
}
return x;
}

等待和通知:

在插入操作中,如果队列已满,线程会在notFull条件变量上等待。

在删除操作中,如果队列为空,线程会在notEmpty条件变量上等待。

插入或删除操作成功后,会相应地通知等待的线程。

/rizcvgg0mxqp9iv7>

HashMap底层

发表于 2024-10-10 | 更新于 2025-06-22 | 分类于 java
字数统计 | 阅读时长

HashMap是Java中一种非常重要的数据结构,它基于哈希表的原理,实现了对键值对(key-value pair)的快速存储和访问。下面详细解释HashMap的数据结构以及其在JDK 1.8中的变化。

HashMap的基本数据结构

在HashMap中,数据是通过“数组+链表”的方式存储的,这种结构也被称为“链表的数组”。具体来说:

  1. 数组:HashMap内部维护了一个Entry数组(在JDK 1.8及以后版本中,Entry被Node类替代,但功能相同),数组的每个元素都是一个桶(Bucket),桶中存放的是指向链表节点的引用。

  2. 链表:当发生哈希冲突(即不同的key通过哈希函数计算得到的下标相同)时,这些key-value对会被存储在同一桶中的链表中。链表中的每个节点都包含key、value、next等字段,用于存储数据和指向下一个节点的引用。

哈希函数与冲突解决

  1. 哈希函数:HashMap通过哈希函数(hash()方法)将key转换为数组的下标。这个哈希函数的目标是尽量均匀地分布元素,以减少哈希冲突的发生。

  2. 冲突解决:当发生哈希冲突时,HashMap采用链地址法(也称为拉链法)来解决。即在同一桶中维护一个链表,将冲突的元素依次添加到链表中。

JDK 1.8中的优化

在JDK 1.8中,HashMap进行了重要的优化,引入了红黑树来进一步减少哈希冲突对性能的影响。具体来说:

  1. 链表转红黑树:当桶中的链表长度超过一定阈值(默认为8)时,HashMap会将这个链表转换为红黑树。红黑树是一种平衡二叉搜索树,具有更高的查找效率(O(log n)),从而提高了HashMap的性能。

  2. 阈值调整:在JDK 1.8中,HashMap还引入了一个新的参数treeifyThreshold来控制链表转换为红黑树的阈值,以及untreeifyThreshold来控制红黑树转换为链表的阈值(当链表长度小于等于6时)。此外,还有一个minTreeifyCapacity参数,用于确保在链表转换为红黑树之前,HashMap的容量至少达到一定的阈值(默认为64),以避免频繁的树化操作。

总结

HashMap的数据结构是基于“数组+链表”的,通过哈希函数将key映射到数组的下标,并通过链表解决哈希冲突。在JDK 1.8中,HashMap引入了红黑树来优化性能,当链表长度超过一定阈值时,会将链表转换为红黑树以提高查找效率。这些优化使得HashMap在处理大量数据时具有更高的性能和更好的稳定性。

HashMap、Hashtable和ConcurrentHashMap

发表于 2024-10-10 | 更新于 2025-06-22 | 分类于 java
字数统计 | 阅读时长

在Java中,HashMap、Hashtable和ConcurrentHashMap都是用于存储键值对的数据结构,但它们在使用场景和内部实现上有显著的区别。下面是对这些区别的详细解释:

线程安全

  1. HashMap:

    • 非线程安全:HashMap没有考虑线程安全问题,如果在多线程环境下不加同步地使用HashMap,可能会导致数据不一致的问题,如死锁、数据丢失或覆盖。
  2. Hashtable:

    • 线程安全:Hashtable是线程安全的,它的所有方法都是同步的。这意味着在多线程环境中,同时访问和修改Hashtable不会导致数据不一致。但是,由于每个方法都使用同步,这可能导致在高并发环境下性能较低。
  3. ConcurrentHashMap:

    • 线程安全且高效:ConcurrentHashMap是为了解决在高并发环境下HashMap的性能问题和Hashtable的同步性能瓶颈而设计的。
    • JDK 1.7及之前:使用分段锁(Segment Lock)机制,将整个哈希表分为多个段(Segment),每个段本质上是一个小的哈希表,并且有自己的锁。这样,在高并发情况下,只要多个线程访问的是不同的段,就可以并行处理,提高了并发性能。
    • JDK 1.8及之后:取消了分段锁,而是采用了CAS(Compare-And-Swap)操作和synchronized关键字来实现更细粒度的锁。每个桶(Node)的锁控制更加精确,只有在必要时才锁定相关的桶,进一步提高了并发性能。

继承关系

  1. Hashtable:

    • 继承自java.util.Dictionary类,这是一个抽象类,提供了基本的键值对存储功能。Hashtable是最早的键值对存储实现之一,因此在Java早期版本中广泛使用。
  2. HashMap和ConcurrentHashMap:

    • 都继承自java.util.AbstractMap抽象类,并实现了java.util.Map接口。AbstractMap提供了Map接口的部分实现,以减少子类实现的工作量。
    • HashMap和ConcurrentHashMap都提供了Map接口的所有功能,但在实现上有所不同,以适应不同的使用场景。

使用场景

  • HashMap:适用于单线程环境或不需要考虑线程安全的情况,因为它提供了最好的性能。
  • Hashtable:适用于多线程环境,但需要同步(即线程安全)的键值对存储。然而,由于性能原因,现在通常推荐使用ConcurrentHashMap。
  • ConcurrentHashMap:适用于高并发环境,提供了线程安全的键值对存储,并且性能优于Hashtable。

总之,选择哪种Map实现取决于应用程序的具体需求,包括是否需要线程安全以及并发性能的要求。

List中的遍历方式

发表于 2024-10-10 | 更新于 2025-06-22 | 分类于 java
字数统计 | 阅读时长
  • 在Java中,遍历并修改一个List集合是一个常见的操作,但如果不小心处理,可能会遇到ConcurrentModificationException异常,这通常是因为在遍历过程中直接修改了集合的结构(如添加、删除元素)。下面是你提到的几种处理方案及其详细解释:
  1. 通过普通的for循环(不建议,可能会漏删):
    使用普通的for循环遍历List,并通过索引直接修改或删除元素。这种方法的问题是,如果删除元素后没有相应地调整索引(例如,连续删除元素时),可能会导致跳过某些元素的检查或索引越界异常。

  2. 通过普通的for循环进行倒序遍历:
    倒序遍历可以避免因删除元素而导致的索引调整问题,因为即使删除了元素,后面的元素索引也不会改变(相对于当前遍历方向)。这种方法可以有效避免漏删的问题。

  3. 使用迭代器循环:
    迭代器提供了一种安全的方式来遍历并修改集合。使用迭代器的remove方法可以安全地删除当前元素,而不会触发ConcurrentModificationException。注意,不能使用集合的remove方法,而应该使用迭代器的remove方法。

  4. 复制列表:
    创建一个原始列表的副本,遍历原始列表,同时在副本上进行删除操作。这种方法是fail-safe的,因为遍历和修改发生在不同的列表上。但这种方法相对复杂,且需要额外的内存来存储副本。此外,如果列表中的元素是复杂对象,可能需要正确地实现equals和hashCode方法以确保正确的删除。

  5. 使用并发安全的集合类:
    使用如CopyOnWriteArrayList这样的线程安全的集合类。这种集合类在修改时创建集合的副本,因此遍历和修改可以安全地进行,但这也可能导致在大量修改时性能下降,因为每次修改都需要复制整个集合。

  6. 使用Stream的过滤方法:
    Java 8引入的Stream API提供了一种声明性的方式来处理集合。通过filter方法,可以创建一个新的Stream,只包含满足特定条件的元素。然后,可以使用collect方法将结果收集到一个新的列表中。这种方法简单高效,因为它避免了在遍历过程中直接修改原始集合。

  7. 通过removeIf方法:
    Java 8的List接口引入了removeIf方法,它接受一个谓词(Predicate),并删除所有满足该谓词的元素。这是一个非常简洁和高效的方法,用于根据条件删除元素。

综上所述,每种方法都有其适用的场景和优缺点。在选择方法时,应考虑集合的大小、修改的频率、内存使用以及对性能的要求。对于大多数情况,使用removeIf或Stream的filter方法是推荐的做法,因为它们既简单又高效。

<i class="fa fa-angle-left"></i>1…9101112<i class="fa fa-angle-right"></i>

232 日志
18 分类
28 标签
GitHub
© 2025 javayun
由 Hexo 强力驱动
主题 - NexT.Gemini