多线程里共享列表、字典?一不小心就翻车!Java 和 Python 各自如何应对?一文看懂。
一、问题:线程不安全,会怎样?
在多线程环境中,如果多个线程同时修改同一个容器(如 List、Dict、Set),轻则数据错乱,重则程序崩溃。
下面用两段代码分别展示 Java 和 Python 中的“翻车现场”。
Java 示例:ArrayList 并发修改
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
publicclassUnsafeDemo{
publicstaticvoidmain(String[] args)throws InterruptedException {
List<Integer> list = new ArrayList<>();
ExecutorService exec = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
exec.submit(() -> list.add(1));
}
exec.shutdown();
Thread.sleep(1000);
System.out.println("Size: " + list.size()); // 很可能小于 1000,甚至抛出异常
}
}
Python 示例:list 并发修改
import threading
lst = []
defadd_one():
for _ in range(100):
lst.append(1)
threads = [threading.Thread(target=add_one) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(len(lst)) # 几乎总是小于 1000,偶尔 IndexError
结论:两者都存在线程安全问题,必须主动处理。
二、Java 的解决方案与优缺点
Java 提供了三种主要思路:
1. 同步包装器
List<Integer> syncList = Collections.synchronizedList(new ArrayList<>());
- 缺点:粒度粗(整个方法锁),并发性能差;迭代时仍需手动加锁。
2. 专用并发容器(推荐)
Map<String, String> map = new ConcurrentHashMap<>();
List<String> copyList = new CopyOnWriteArrayList<>();
- 优点:高并发性能极佳。例如
ConcurrentHashMap 采用分段锁或 CAS,读操作无锁;CopyOnWriteArrayList 适合读多写少。 - 缺点:部分容器内存占用稍高(如
CopyOnWriteArrayList 每次修改复制整个数组)。
3. 手动锁(不推荐用于容器)
List<Integer> list = new ArrayList<>();
synchronized (list) {
list.add(1);
}
Java 优点总结:
- 无 GIL 束缚,多线程可真正并行,容器设计充分挖掘 CPU 性能。
缺点:
- 开发者需要主动选择正确的实现,误用
ArrayList 后果严重。
三、Python 的解决方案与优缺点
受 GIL(全局解释器锁)影响,Python 多线程并行计算受限,但容器的线程安全问题依然存在——GIL 只保证字节码级别的原子性,list.append() 并非原子操作。
1. 使用锁(最直接)
import threading
lst = []
lock = threading.Lock()
defadd_one():
for _ in range(100):
with lock:
lst.append(1)
# 启动10个线程,最终长度正确为1000
- 缺点:锁粒度通常很大,且 Python 多线程本身就难以提升 CPU 密集型任务的性能。
2. 使用队列(推荐生产-消费模式)
import threading
import queue
q = queue.Queue() # 线程安全
defworker():
q.put(1) # 无需额外加锁
for _ in range(1000):
threading.Thread(target=worker).start()
- 优点:
queue.Queue 内部已处理好线程安全,且天然解耦。
3. 多进程代替多线程(终极方案)
from multiprocessing import Pool
defadd_one(x):
return x + 1
with Pool(10) as p:
result = p.map(add_one, range(1000))
- 优点:每个进程有独立内存空间,完全规避共享容器的线程安全问题,且能利用多核。
Python 优点总结:
- 简单场景下,GIL 让纯读操作天然安全(但绝不应依赖)。
queue、multiprocessing 提供了安全的高层抽象。
缺点:
- 没有像
ConcurrentHashMap 这样的高性能并发容器,大多只能手动加锁。 - GIL 导致多线程容器的竞争问题实际影响小于 Java,但也因此官方投入较少,生态相对薄弱。
四、对比总览
| | |
|---|
| | |
| java.util.concurrent | threading.Lock |
| | |
| | |
| | |
| | |
五、总结说明
Python内置的list、dict、set等本身不是线程安全的。多线程下同时修改,同样会出现数据损坏(如list内部指针错乱、dict丢失条目)。Java(注重并发原生支持)
- 设计定位:从诞生起就内置强大线程模型,容器明确区分安全与非安全实现。
- 解决方案:提供
ConcurrentHashMap、CopyOnWriteArrayList等高并发专用容器,以及Collections.synchronizedXXX包装器。 - 优缺点:方案成熟强大(特别是
Concurrent系列,高并发性能极佳),但需要开发者主动选择正确的实现,选错或自己用synchronized很容易成为性能瓶颈。
Python(受限于GIL)
- 设计定位:虽有
threading模块,但全局解释器锁(GIL)限制了纯Python代码的并行能力。 - 用锁保护:与Java类似,使用
threading.Lock。 - 用队列:优先用
queue.Queue或multiprocessing.JoinableQueue,它们内部已处理好线程安全。 - 换用进程:计算密集型任务用
multiprocessing,可绕过GIL。
- 优缺点:简单的读操作由于GIL,有时能偶然避免数据损坏(但绝不能依赖,写操作依然危险)。缺点是可用高并发容器很少,大多只能手动加锁;而且GIL让多线程并行计算受限,导致容器的线程安全问题在Python中不如在Java中普遍。
核心建议
Java:优先看java.util.concurrent包。能用ConcurrentHashMap就不要用Hashtable或Collections.synchronizedMap。Python:
- 优先用
queue.Queue等自带安全的通信结构。 - 必须操作共享
list/dict时,用Lock保护。 - 考虑用
multiprocessing替代threading,进程有独立内存空间,每个进程有自己的数据副本。
六、一句话建议
- Java 开发者:忘掉
Hashtable,拥抱 ConcurrentHashMap 和 CopyOnWriteArrayList。 - Python 开发者:优先用队列或多进程;必须共享
list/dict 时,记得加锁。
最终结论:无论 Java 还是 Python,都不要假设你的容器在多线程下是安全的——主动防护,才是好习惯。