社交媒体

线程安全

线程安全

钩子:两万次加法,少了一万多

第 8 篇留过一个线头:i = i++ 之后 i 还是老值。当时说那是单线程里的坑。

今天把它搬到两个线程里,事情会离谱得多。我做了一个实验:两个线程,各对同一个变量执行一万次 count++。

你猜结果是多少?按直觉应该是 20000。

双线程结果: 12945(期望 20000,实际 12945)
>> 少了 7055 次!

不是随机波动,是稳定地少。跑多少次都不到 20000。这就是并发编程的第一课:线程安全。

33-双线程i++丢失更新图.png

核心:i++ 不是一步,是三步

count++ 看着是一行,到了 CPU 层面是三个动作:

① 读:把 count 的值从内存读到寄存器
② 加:寄存器里 +1
③ 写:把结果写回 count 的内存位置

单线程没问题,因为这三步永远排着队来。但两个线程一起跑,随时可能被插队:

线程A:读到 count=100 →(被抢走 CPU)
线程B:读到 count=100 → 加1 → 写回 101
线程A:(抢回 CPU)加1 → 写回 101   ← 它手里还是那张 100 的旧票根

两次加法,只留下一次的效果。每发生一次这样的"撞车",总数就少 1。一万次里撞几千次很正常。

这种现象叫"丢失更新"(Lost Update)。 单看每一行代码都没错,错的是"三步可以被拆开"这件事本身。

药方一:synchronized——排队上厕所

最直接的思路:让这三步变成"一次只能一个人做"。Java 给的关键字就是 synchronized:

static int safeCount = 0;

static synchronized void addSafe() {
    safeCount++;   // 拿到锁的线程才能进来,其他人门口等着
}

同一时刻只有一个线程能进这个方法,进去的干完(读→加→写全做完)才轮到下一个。实测结果:20000,一分不少。

代价是排队:并发越高,等得越久。锁是把双刃剑——保正确,牺牲速度。

药方二:AtomicInteger——不用排队的自动门

排队不是唯一的办法。Java 还准备了一批"原子类",比如 AtomicInteger:

import java.util.concurrent.atomic.AtomicInteger;

static AtomicInteger count = new AtomicInteger(0);

// 加 1 是一个不可分割的原子操作,硬件层面保证不撞车
count.incrementAndGet();

它靠 CPU 的特殊指令保证"读加写"一步到位,不需要排队上锁。实测同样精确到 20000,高并发下性能更好。

新手阶段记住一句话就够:多个线程共改一个变量,就要想办法保护它——先会 synchronized,后面再认识原子类。

速查表

要点 内容
线程安全 多线程访问共享数据,结果依然正确
根因 i++ 是"读→加→写"三步,会被插队
现象 丢失更新:加了 2 万次,实际只剩 12945
药方一 synchronized:同一时刻只放一个线程进
药方二 AtomicInteger:原子操作,不用排队
判断标准 有没有多线程 + 共享变量 + 写操作,三个凑齐就要防

本文示例代码均已本地实测通过,可直接运行。
对应文件:ThreadSafeDemo.java


橙码酱,分享更有用的技术知识,助你成为更优秀的开发者。