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

👌为什么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>

 wechat
天生我才必有用