7. 线程安全类
概念
普通 HashMap、ArrayList 和 Python 的 dict、list 允许多个线程同时读,但不允许在没有协调的情况下同时改。两个线程同时执行 count = count + 1,结果可能少加一次。 Java 并发包把常见需求做成了线程安全类:AtomicInteger 做计数,ConcurrentHashMap 做映射,BlockingQueue 做移交。Python 标准库里 Queue 是线程安全的;自己保护一个 dict 时,用同一把 Lock 包住读改写。
- 原子类适合单个数值。compareAndSet 可以在不加重锁的情况下完成判断并更新。
- ConcurrentHashMap 的单个方法是线程安全的,但“先查再放”这种组合动作仍要用 compute 一次做完。
- 阻塞队列同时解决移交和等待:队列空时 take 休眠,队列满时 put 休眠。
- 线程安全类缩小了加锁范围,不是所有共享数据都可以不加思考地并发写。
算法流程
下面的 UML 图说明 Java 例程和 Python 例程共同实现的流程。实线箭头表示调用或发送,虚线箭头表示返回或唤起。
并发计数(UML 时序图)
sequenceDiagram
participant T1 as 线程 1
participant Counter as 原子计数或同一把锁
participant Map as 并发映射
participant T2 as 线程 2
T1->>Counter: 总次数加一
T2->>Counter: 总次数加一
Note over Counter: 两次更新都生效,不会丢掉其中一次
T1->>Map: 没有该用户就创建,再把次数加一
T2->>Map: 同一次原子操作更新同一用户
Note over Map: 先查再放要合成一步,避免后写入覆盖先写入
Java 例程
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class SafeClasses {
static final AtomicInteger HITS = new AtomicInteger(); // 总次数。incrementAndGet 不会丢更新
static final ConcurrentHashMap<String, AtomicInteger> BY_USER =
new ConcurrentHashMap<String, AtomicInteger>(); // 分段并发,读多写多时不必锁整张表
static void hit(String user) {
HITS.incrementAndGet();
// computeIfAbsent 把“没有就创建”做成一次原子操作。
// 如果写成先 get 再 put,两个线程可能各放进一个计数器,其中一个被覆盖。
BY_USER.computeIfAbsent(user, k -> new AtomicInteger()).incrementAndGet();
}
}
Python 例程
import threading
from queue import Queue
hits = 0
by_user = {} # 普通 dict 不是线程安全的,写它必须加锁
lock = threading.Lock()
tasks = Queue() # Queue 自己有锁,put/get 不需要再包一层
def hit(user):
global hits
with lock: # 总次数和分用户次数要一起改,所以放在同一临界区
hits += 1
by_user[user] = by_user.get(user, 0) + 1
def worker():
while True:
user = tasks.get() # 队列空时,这个线程在这里休眠
if user is None: # None 是约定的结束标记,不是业务数据
break
hit(user)
tasks.task_done() # 和一次 get 配对,供 join 统计未完成任务