关于i++是不是原子操作的问题

本文探讨Java中i++和++i是否为原子操作。在单核CPU中,能在一个指令间完成的操作可视为原子操作,但多核CPU下,处理器不能保证复杂内存操作的原子性。通过代码测试发现i++和++i并非原子操作,还介绍了总线锁和缓存锁机制来保证其原子性及适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

i++是不是原子操作?这个看似简单的问题,实则背后有很深的坑,今天就来踩踩这个坑。
之所以要讨论某个操作是不是原子操作,是因为一旦确认某个操作是原子操作的话,就不用为了去保护这个操作而加上昂贵又要耗费性能的锁。而在单核CPU中,中断只会发生在指令间,可以把能够在一个指令间完成的操作,看成是原子操作。
处理器虽然会自动保证基本的内存操作的原子性,但对于多核CPU的复杂的内存操作,处理器是不能保证其原子性的。这里举个i++的例子,因为i++是个经典的读改写操作。
首先运行一下代码:
这里写图片描述
运行这段代码的结果大部分情况下都是1000,这样i++的操作好像是原子操作的,而这也是我们所期望的值。但是当把循环次数增加10倍,100倍甚至更大的时候,每次所得的结果却都是不一样的。当我把上述代码稍作修改之后:
这里写图片描述
所得的结果:
这里写图片描述
同样的代码,只是加了一点其他的操作,结果就大不一样了,而且把循环次数或线程数增大,看到的结果更加是五花八门。为什么会出现这样的结果呢?其实从这里可以看出i++确实不是原子操作。
I++做了三次指令操作,两次内存访问,第一次,从内存中读取i变量的值到CPU的寄存器,第二次在寄存器中的i自增1,第三次将寄存器中的值写入内存。这三次指令操作中任意两次如果同时执行的话,都会造成结果的差异性。而对于++i,在多核机器上,CPU在读取内存时也可能同时读到同一个值,这样就会同一个值自增两次,而实际上只自增了一次,所以++i也不是原子操作。对于上述代码中第一次出现的结果,实际上是个原子操作的假象,因为线程数量少,循环次数也少,加上在执行时,虚拟机可能会对代码做部分优化,所以看起来结果是对的,而当我在代码中做一些其他的耗时的操作时,这种假象就不攻自破了。
那么,如何实现i++和++i的原子性呢?
在多核CPU的复杂内存操作中,处理器提供了总线锁和缓存锁两个机制来保证原子性,可以通过处理器提供的很多LOCK前缀的指令来实现。
总线锁:多个处理器可能会同时从各自的缓存中读取变量,并分别进行操作,在分别写入内存中,想要保证读改写共享变量的操作是原子的,就要使用总线锁来解决,即使用处理器提供的一个LOCK#信号,当有一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞,此时该处理器可以独占共享内存。
缓存锁:频繁使用的内存将会缓存在处理器的高速缓存里,内存区域如果被缓存到处理器的缓存行里,并且在Lock操作期间被锁定,那么当他执行锁操作回写到内存时,其他处理器会检查各自缓存行内的内存地址,如果发现自己的缓存行对应的地址被修改了,就会将缓存行置于无效状态,下次访问时,重新从内存中读取数据到缓存行,并允许它的缓存一致性机制(通常采用嗅探技术来实现,即缓存不仅仅是在内存传输的时候才和总线打交道,而是时刻不停的在窥探总线上发生的数据交换,并跟踪其他缓存在做什么,所以当一个缓存代表它所属的处理器去读写内存时,其他处理器都会得到通知,从而保证各个缓存保持同步)来保证操作的原子性。此时,处理器不会在总线上输出LOCK#信号。
总线锁会把CPU和内存之间的通信锁住,导致其他处理器不能操作其他内存地址的数据,所以总线锁的开销比较大,在某些场合下,会使用缓存锁来代替总线锁进行优化。
但是,当操作的数据不能被缓存在处理器内部或要操作的数据会跨多个缓存行时,处理器会调用总线锁。对于有些处理器不支持缓存锁,就算锁定的内存区域在处理器的缓存行,这时也会调用总线锁。

上一篇:Java中是如何实现原子操作的?
下一篇:多线程的几种实现方式

### 关于 `i++` 的原子性 在 C/C++ 中,`i++` 并不是一个原子操作。它实际上由三个独立的操作组成:加载当前值、增加该值以及存储新的值到变量中[^1]。这些操作通常不会作为一个整体被执行,而是被分解成多个指令,在多线程环境中可能会引发竞态条件。 为了使类似的自增操作具备原子性,C++ 提供了 `<atomic>` 头文件中的 `std::atomic` 类型及其成员函数。通过使用 `std::atomic<int> i;` 定义变量,并调用其提供的方法(如 `fetch_add()` 或 `operator++()`),可以确保整个操作在线程之间保持一致性和不可分割性。 下面是一个简单的例子展示如何利用 `std::atomic` 实现真正的原子递增: ```cpp #include <iostream> #include <thread> #include <atomic> std::atomic<int> counter(0); void increment() { for (int i = 0; i < 1000; ++i) { counter++; // 使用 std::atomic 的 operator++() } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Final counter value: " << counter.load() << std::endl; } ``` 在这个程序里,即使两个线程同时访问并修改同一个全局变量 `counter`,由于它是声明为 `std::atomic<int>` 类型的,所以每次调用 `counter++` 都会以一种线程安全的方式完成加一动作。 另外需要注意的是,默认情况下所有的 atomic 操作都遵循顺序一致性 (`memory_order_seq_cst`) ,这意味着除了保证单个操作本身的原子性之外,还维护了一个全系统的偏序关系来协调不同处理器核心上的缓存刷新行为[^3]。不过也可以根据具体需求调整至更弱一些的记忆体排序选项比如 `memory_order_relaxed`, 这样虽然可能提高性能但是失去了某些同步保障。 综上所述,普通的整数类型上的 `i++` 不具有内在的原子特性;而借助标准库里的 `std::atomic` 则能轻易达成这一目标。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值