原子性:不可分割的操作

private int count = 0;

public void test() {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Thread t = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
count += 1;
}
});
ts.add(t);
} // ...start启动线程,join等待线程
assert count == 100 * 10000;
}

对于Java这样的高级语言,一条语句最终会被转换成多条处理器指令完成,例如上面代码中的count += 1,至少需要三条处理器指令。

1)指令1:把变量count从内存加载到处理器的寄存器;

2)指令2:在寄存器中执行+1操作;

3)指令3:将结果写入内存(缓存机制导致可能写入的是处理器缓存而不是内存)。

那么假设有两个线程A和B,同时执行 count+=1,可能存在如下情况:

1)线程A从内存加载count并执行count+=1,同时线程B从内存加载count并执行count+=1,并同时写回内存。那么这时结果是:count = 1。

2)线程A从内存加载count并执行count+=1,并将count结果写回内存。线程B再从内存加载count并执行count+=1。那么这时结果是:count = 2。

可以看到如果要count结果正确,要保证count读取、操作、写入三个过程不被中断。这个过程,可以称之为原子操作。原子 (atomic)本意是“不能被进一步分割的最小粒子”,而原子操作 (atomic operation) 意为“不可被中断的一个或一系列操作”。

处理器通过总线锁定、缓存锁定和原子指令等方式实现原子操作。

1)总线锁定(Bus Locking):通过LOCK指令锁住总线BUS,使当前处理器独享内存空间。但是此时其他处理器都不能访问内存其他地址,效率低。

2)缓存锁定(Cache Locking):现代处理器主要依赖缓存一致性协议(如MESI)实现原子操作。当处理器核心执行LOCK指令时,会先尝试锁定目标内存地址所在的缓存行。通过MESI协议,处理器核心将缓存行置为独占状态(Exclusive/Modified),阻止其他处理器核心修改。操作完成后,缓存行状态更新并释放锁,其他核心可重新获取该行。若内存操作跨越两个缓存行(如未对齐的8字节写入),或目标地址未被缓存时,需直接锁总线。

3)原子指令(Atomic Instruction):处理器通常提供一些特殊的指令来实现原子操作,例如,x86架构的CMPXCHG(比较并交换)指令,ARM架构的LDREX和STREX(加载和存储独占)指令。

在实际的并发编程中,缓存一致性协议和原子操作通常需要一起使用。例如,CMPXCHG只在单核处理器下有效,多核处理器依然要加上LOCK前缀(LOCK CMPXCHG)。

当处理器执行CMPXCHG指令时,它会先将需要操作的内存内容加载到缓存中,然后锁定这部分缓存,执行比较和交换操作,最后将结果写回内存。在这个过程中,其他的处理器不能访问被锁定的缓存,从而保证了操作的原子性。

compxchg [ax] (隐式参数,EAX累加器), [bx] (源操作数地址), [cx] (目标操作数地址)

利用CMPXCHG指令可以通过循环CAS方式来实现原子操作。

// 判断当前机器是否是多核处理器
int mp = os::is MP();
_asm {
mov edx, dest
mov ecx, exchange value
mov eax, compare_value
// 这里需要先进行判断是否为多核处理器
LOCK IF MP(mp)
// 如果是多核处理器就会在这行指令前加Lock标记
cmpxchg dword ptr [edx],ecx
}

CAS(Compare and Swap)是一种常用的原子操作。CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

Java语言提供了大量的原子操作类,来实现对应的CAS操作。比如AtomicBoolean,AtomicInteger,AtomicLong等。

private AtomicInteger count = new AtomicInteger(0);

public void test() {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Thread t = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
count.incrementAndGet();
}
});
ts.add(t);
} // ...start启动线程,join等待线程
assert count.get() == 100 * 10000;
}

尽管CAS操作是原子的,但它也存在一些问题,主要包括以下几个方面。

1)ABA问题:在CAS操作中,如果一个值在操作开始时是A,然后被改为B,最后又被改回A,那么CAS操作会误认为没有发生变化。为了解决ABA问题,可以使用版本号或标记来跟踪变化。

2)自旋开销:CAS操作是通过自旋来实现的,即不断尝试进行CAS操作直到成功或达到一定的尝试次数。如果CAS操作失败,线程会一直自旋等待,这会消耗处理器资源,会影响系统的性能。

3)只能保证一个变量的原子性:CAS操作只能保证一个变量的原子性,如果需要保证多个变量的一致性,需要使用其他的同步机制。

未完待续

很高兴与你相遇!如果你喜欢本文内容,记得关注哦!!!

为什么i++不是原子操作?一个让无数并发程序崩溃的“常识”的更多相关文章

  1. IM服务器:编写一个健壮的服务器程序需要考虑哪些问题

    如果是编写一个服务器demo,比较简单,只要会socket编程就能实现一个简单C/S程序,但如果是实现一个健壮可靠的服务器则需要考虑很多问题.下面我们看看需要考虑哪些问题. 一.维持心跳 为何要维持心 ...

  2. Linux环境下部署完JDK后运行一个简单的Java程序

    前言 前一篇文章详细讲解了如何在Windows环境下安装虚拟机+Linux系统,并且成功部署了JDK. 不过部署完JDK之后,我们判断部署是否成功的依据是看"java -version&qu ...

  3. 编写一个简单的C++程序

    编写一个简单的C++程序 每个C++程序都包含一个或多个函数(function),其中一个必须命名为main.操作系统通过调用main来运行C++程序.下面是一个非常简单的main函数,它什么也不干, ...

  4. 一个优秀windows C++程序员的知识体系

    思考一个优秀windows C++ 程序员该有哪些知识,可最终发现什么知识都不能少, 看下图: 除了上面知识,程序员还要不断学习, 保持对新知识的热情. 转自http://www.cppblog.co ...

  5. 一个python爬虫小程序

    起因 深夜忽然想下载一点电子书来扩充一下kindle,就想起来python学得太浅,什么“装饰器”啊.“多线程”啊都没有学到. 想到廖雪峰大神的python教程很经典.很著名.就想找找有木有pdf版的 ...

  6. 使用Go开发一个简单的服务器程序

    最近有个小项目,需要一个简单的后台程序来支撑,本来想用Nodejs来做,但是由于本人js一直很菜,并且很讨厌callback,虽然我也很喜欢异步模型,但我一直都觉得JS是反人类的.后台就用了go处理, ...

  7. 一个优秀windows C++程序员的知识体系[转]

    转自:一个优秀windows C++程序员的知识体系 思考一个优秀windows C++ 程序员该有哪些知识,可最终发现什么知识都不能少, 看下图: 除了上面知识,程序员还要不断学习, 保持对新知识的 ...

  8. 做一个聪明的.net程序员

    最近看了传智播客(http://net.itcast.cn/)的.net培训视频,感受颇深,忍不住要把感受写下来跟网友分享一下. 我从接触.net到现在已经至少过去了三五个年头,用.net也已经做了若 ...

  9. 在linux下,查看一个运行中的程序, 占用了多少内存

    1. 在linux下,查看一个运行中的程序, 占用了多少内存, 一般的命令有 (1). ps aux: 其中  VSZ(或VSS)列 表示,程序占用了多少虚拟内存. RSS列 表示, 程序占用了多少物 ...

  10. Hadoop学习历程(四、运行一个真正的MapReduce程序)

    上次的程序只是操作文件系统,本次运行一个真正的MapReduce程序. 运行的是官方提供的例子程序wordcount,这个例子类似其他程序的hello world. 1. 首先确认启动的正常:运行 s ...

随机推荐

  1. 多数据库迁移的艺术:Alembic在复杂环境中的精妙应用

    title: 多数据库迁移的艺术:Alembic在复杂环境中的精妙应用 date: 2025/05/11 00:35:52 updated: 2025/05/11 00:35:52 author: c ...

  2. argparse基本功能极简介绍

    argparse基本功能极简介绍 python脚本文件可以通过命令行的方式调用,在这种调用方法中,可以通过sys.argv来把命令行参数传入脚本文件,通过这种方式传入的参数是string,并且需要将该 ...

  3. Hitachi Vantara Programming Contest 2024(AtCoder Beginner Contest 368)题解A~D

    A - Cut 题意: 将数组的后k个字符移到前面 思路: 可以用rotate()函数让数组中的元素滚动旋转 rotate(v.begin(), v.begin() + n - k, v.end()) ...

  4. 在LLVM中的greedy Register Allocation pass代码详解

    LLVM 贪婪寄存器分配器(RAGreedy)详细处理流程 日期: 2025年5月29日 摘要 本文深入分析 LLVM 贪婪寄存器分配器(RAGreedy)的处理流程,详细描述从优先级队列获取虚拟寄存 ...

  5. Target JRE version (1.8.0_201) does not match project JDK version (java version "1.7"), will use sources from JDK: 1.8

    问题描述:IntelliJ IDEA 启动项目时,常常提示Target JRE version (1.8.0_201) does not match project JDK version (java ...

  6. LUNARiA

    本文同步发布于我的网站 也算是头一次在没有任何安利和剧透,仅在看了简介的情况下就直接下单并开始游玩一部gal了.果然,没有给我留下什么遗憾呢. 游玩日志 SKYOUT-FOREVER <LUNA ...

  7. Vue 学习笔记 [Part 2]

    作者:故事我忘了¢个人微信公众号:程序猿的月光宝盒 目录 一. 计算属性 1.1. 计算属性的本质 1.2. 计算属性和methods对比 〇.ES6补充 0.1. let/var 0.2 const ...

  8. python爬虫学习——re模块(正则表达式)

    正则表达式常用操作符 . 任何单个字符 [] 字符集,对单个字符给出取值范围 [abc].[a-z] [^ ] 非字符集,对单个字符给出排除范围 [^abc] * 对前一个字符0次或无限次拓展 abc ...

  9. Kubernetes二进制文件下载链接

    这里,我们下载kubernetes的二进制文件在其官网下载即可: 具体链接:https://kubernetes.io/docs/setup/release/notes/

  10. 现在的AI工具还能写剧本杀了?

    本文由 ChatMoney团队出品 近年来,剧本杀作为一种新兴社交游戏,收到了越来越多人的喜爱,它不仅需要玩家们发挥自身演技,还需运用逻辑思维推理,分析所获得的线索,找出案件真凶.然而你是否想过,你在 ...