多线程读写普通int变量问题
场景:线程A写普通
int,线程B读同一个普通int,无锁、无volatile。前提:int在当前平台是32位,对齐访问。
1、会不会出现"字节撕裂(半写值)"?
- x86/x64:对齐的32位int,读写是原子的CPU指令,写的时候不会读到一半字节,不会读到一个一半旧一半新的畸形数值。
⚠️ 注意:64位long long在32位系统就不是原子,会出现撕裂;但32位对齐int不会。
但!原子 ≠ 线程安全!这是最容易踩坑的地方。
虽然不会读到破碎值,但依然有两个严重问题:可见性问题 、指令重排序问题。
2、可见性问题(CPU缓存)
每个CPU核心有私有L1/L2缓存。
- 线程A(CPU0)修改int,新值先写到CPU0缓存,不一定立刻刷回主内存。
- 线程B(CPU1)读变量,读的是CPU1自己缓存里的旧副本。
现象:
A已经把
x=100,B可能长期读到旧值x=0,永远看不到新修改。
这就是缓存可见性失效。
3、编译器 / CPU指令重排序
编译器、CPU会为性能打乱指令执行顺序,只要单线程逻辑不变。
示例伪代码:
c
int flag = 0;
int data = 0;
//线程A
data = 123;
flag = 1; //通知线程B数据准备好了
//线程B
if(flag == 1){
print(data);
}
重排序后,线程A实际执行顺序可能变成:
c
flag = 1;
data = 123;
线程B看到flag=1,去读data,读到还没更新的旧值。
普通int没有任何屏障,编译器和CPU都可以重排读写。
4、总结现象(普通int,无volatile无锁)
- ✅ 对齐32‑bit int:不会发生值撕裂,不会读到半个更新的乱码数字
- ❌ 读线程可能一直读到旧值(缓存可见性)
- ❌ 指令重排序,读写顺序被打乱,业务逻辑错乱
- ❌ 不能保证读到最新写入的值
很多人误以为:int读写原子就不用锁,这是错误。原子只保证不会读到破碎字节,不保证可见性、不保证顺序。
5、怎么解决
C/C++
volatile int x- 阻止编译器优化,但是不能阻止CPU硬件重排序、缓存同步,现代多核下不完全可靠,不推荐用于线程同步。
std::atomic<int> x✅推荐- 硬件原子读写,自带内存屏障,解决可见性+重排序。读
x.load(),写x.store()。
- 硬件原子读写,自带内存屏障,解决可见性+重排序。读
- 互斥锁mutex保护读写。
Java
volatile int x:禁止重排序、保证可见性;32位int读写原子。
补充边界案例
- 如果int没有内存对齐 (刻意偏移),哪怕32位int,读写不再是CPU原子操作,就会出现值撕裂,读到一半旧一半新的数值。
- 64位long long,32位操作系统,普通读写不是原子,一定会撕裂。
最简记忆
对齐32位int:读写指令原子,但不代表线程安全。原子=不会碎;线程安全=可见+有序。两者不是一回事。
如果你需要,我可以写一小段可复现的C++示例代码,演示"写了但是读不到新值"这个现象。