如果你还没体验过原子操作的好处,请看这篇:https://blog.youkuaiyun.com/silently_frog/article/details/100510946
我们知道原子操作的作用:在多线程中,让一个共享变量的值同步。所以要想理解原子操作,我们可以去理解在多线程中,什么问题导致了共享变量的值不同步。
在多线程中,如果要实现共享的变量值同步需要解决什么问题了???
1. 解决修改变量值后,其他线程是否知道???
2. 解决Java执行顺序的控制
3. 变量能否完整的执行读写等操作???
1. 解决修改变量值后,其他线程是否知道???
这个问题官方称之为----可见性,为啥不可见???
这就是内存模型的知识了,简单说明:
程序运行过程中的临时数据是存放在主存(物理内存)当中的,而运算、代码的执行等命令运行在CUP中。由于CPU处理速度比主存的读写速度快很多。所以就会导致CPU等待主存处理结果的问题,效率低下。因此高速缓存就来了。当线程创建时,会创建一个自己独有的工作内存-----本地内存(将其理解为在高速缓存中开辟了一个空间),将运算需要的数据从主存复制一份到工作内存当中,那么 CPU进行计算时就可以直接从它的工作内存读取数据和向其写入数据,当运算结束之后,再将高速缓存 中的数据刷新到主存当中,而其他线程的工作内存中对应的数据并没有更新。所以每个线程都会有自己独立的工作内存,并保存了一份会用到的数据。这样就会导致变量的不可见,从而导致了不同步。
2. 解决Java执行顺序
这个问题官方称之为----有序性,在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。举个例子:
int a=0;
boolean flag=false;
// 自己写的代码
public void getState(){
a=1;
flag=true;
}
public int add(){
if(flag){
return a+a;
}
}
如果重排序优化后的getState代码 如下:
// 重排序后,优化的代码
public void getState(){
flag=true;
a=1;
}
在单线程中不会有问题,但是在多线程中会导致一个线程刚执行了flag=true,还未执行a=1时,另一个线程就执行了add()函数,导致a还是0.
3. 变量能否完整的执行读写等操作???
这个问题官方称之为----原子性。
原子就是最小不可拆分的,原子操作就是最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成,可能是一个动作或多个动作。而原子性就是保证在不中断的情况下,完整的执行完一个操作。
如加法操作,一个加法操作会被分为三个动作,1.读取变量、2.计算变量、3.更新变量。
一般处理器会自动保证基本的内存操作的原子性,但是在复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。
为啥一个操作可能不完整执行???
举个例子:在多线程中,假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?
一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。可能发生一种情况:当将低16位数值写入之后,高16位还未写入时,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据.即有些操作必须是捆绑一起执行。
注意:volatile 不能保证操作的原子性
----------------------------------------------
上面已经列出了实现共享变量在多线程中的三大要求:可见性、有序性、原子性。
而Java已经帮我们实现了满足三个要求的类-----统称为原子操作。
原子操作是volatile和CAS机制的封装。被volatile修饰的变量会具有可见性、有序性,而CAS机制让其具有原子性。
volatile做了什么???解决了问题1和问题2
问题1的解决:
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现**缓存一致性**协议,**每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期**了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。因此,经过分析我们可以得出如下结论:
a. Lock前缀的指令会引起处理器缓存写回内存;
b. 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
c. 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。
问题2的解决:
编译器在生成字节码时,会在volatile指令序列中插入内存屏障来禁止特定类型的处理器重排序。内存屏障看后面的参考文献 -----https://www.cnblogs.com/yungyu16/p/13200620.html
问题3的解决:
从问题3的例子可以知道,多线程如果同时对变量的操作,会导致错误。所以我们需要给其操作加一把锁,在执行这个操作的时候,其他线程就不能在对这个变量执行操作。
CAS机制:通过硬件命令(在Intel CPU上称为CMPXCHG指令)保证了原子性--------典型的乐观锁(对读不限制,对写限制)。
CAS 操作包含三个操作数 —— 内存位置(V----即主内存中的值)、预期原值(A----- 线程中本地内存存储的值)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
最后提一下面试可能问到的CAS自旋和ABA问题:
由于CAS使用了乐观锁的模式-----所以在写操作时会出现线程抢夺锁的情况。如果没有抢到锁的线程就会自旋,即循环请求锁,直到成功。这样的缺点:一直占用CPU资源。
ABA问题:线程1将变量从A改为了B,线程2将变量B改为了A.而此时线程3会认为变量没有发生改变,在某些业务下,就有问题了。解决方法:使用AtomicStampedReference(,,版本号),每次会让我们传入一个变量版本。
补充:
每个线程都有一个私有的本地内存(如CPU的高速缓存),本地内存中存储了该线程以读/写共享变量的副本。
(重点)本地内存是JMM的一个抽象概念,并不真实存在;它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

Java 内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(比如CPU的高速缓存)。
线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存。
参考文献:
内存屏障:https://www.jianshu.com/p/1ae887521cf3
volatile关键字讲解:https://www.cnblogs.com/dolphin0520/p/3920373.html
https://www.cnblogs.com/awkflf11/p/9218414.html
https://blog.youkuaiyun.com/qq_18331665/article/details/104323566
本文深入探讨了原子操作在多线程环境中的作用,包括可见性、有序性和原子性的概念,以及Java如何通过volatile和CAS机制实现这些特性。
1150

被折叠的 条评论
为什么被折叠?



