文章目录
- 一、什么是原子操作
- 二、Java中原子操作的实现方式
- 2.1使用锁实现原子操作
- 2.2使用CAS实现原子操作
- 2.2.1 CAS实现原子操作的问题
- 三、CPU如何实现原子操作
- 3.1对于单核CPU
- 3.2对于多核CPU
一、什么是原子操作
- 原子操作:一个或多个操作在CPU执行过程中不被中断的特性
- 当我们说原子操作时,需要分清楚针对的是CPU指令级别还是高级语言级别。
- 比如:经典的银行转账场景,是语言级别的原子操作;而当我们说volatile修饰的变量的复合操作,其原子性不能被保证(这里指的是CPU指令级别)。二者的本质是一致的。
- “原子操作”的实质其实并不是指“不可分割”,这只是外在表现,本质在于多个资源之间有一致性的要求,操作的中间态对外不可见。
- 比如:在32位机器上写64位的long变量有中间状态(只写了64位中的32位);银行转账操作中也有中间状态(A向B转账,A扣钱了,B还没来得及加钱)
二、Java中原子操作的实现方式
- 除了long和double之外的基本类型的赋值操作,因为long和double类型是64位的,所以它们的操作在32位机器上不算原子操作,而在64位的机器上是原子操作。
- 所有引用reference的赋值操作
- java.concurrent.Atomic *包中所有类的原子操作
- Java使用锁和自旋CAS实现原子操作
2.1使用锁实现原子操作
- 锁机制保证只有拿到锁的线程才能操作锁定的内存区域。
- JVM内部实现了多种锁,偏向锁、轻量锁、互斥锁。不过轻量锁、互斥锁(即不包括偏向锁),实现锁时还是使用了CAS,即:一个线程进入同步代码时用自CAS拿锁,退出块的时候用CAS释放锁。
-
锁定的临界区代码对共享变量的操作是原子操作。synchronized
2.2使用CAS实现原子操作
- 利用CAS实现原子操作,其实我们在用的时候,是使用java.util.concurrent.atomic包下的各种原子类,这些原子类里面的各种方法底层使用的就是CAS。下面是该包中的类:
- AtomicBoolean – 原子布尔
- AtomicInteger – 原子整型
- AtomicIntegerArray – 原子整型数组
- AtomicLong – 原子长整型
- AtomicLongArray – 原子长整型数组
- AtomicReference – 原子引用
- AtomicReferenceArray – 原子引用数组
- AtomicMarkableReference – 原子标记引用
- AtomicStampedReference – 原子戳记引用
- AtomicIntegerFieldUpdater – 用来包裹对整形 volatile 域的原子操作
- AtomicLongFieldUpdater – 用来包裹对长整型 volatile 域的原子操作
- AtomicReferenceFieldUpdater – 用来包裹对对象 volatile 域的原子操作
- AtomicBoolean – 原子布尔
- AtomicInteger – 原子整型
- AtomicIntegerArray – 原子整型数组
- AtomicLong – 原子长整型
- AtomicLongArray – 原子长整型数组
- AtomicReference – 原子引用
- AtomicReferenceArray – 原子引用数组
- AtomicMarkableReference – 原子标记引用
- AtomicStampedReference – 原子戳记引用
- AtomicIntegerFieldUpdater – 用来包裹对整形 volatile 域的原子操作
- AtomicLongFieldUpdater – 用来包裹对长整型 volatile 域的原子操作
- AtomicReferenceFieldUpdater – 用来包裹对对象 volatile 域的原子操作
- 在这一点可以参考:
- 一个案例:
package com.wlw.cas;
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
//CAS :compareAndSet() 这个方法的缩写 比较并交换!
public static void main(String[] args) {
//原子类的底层运用了CAS
AtomicInteger atomicInteger = new AtomicInteger(2020);
// public final boolean compareAndSet(int expect, int update)
//如果我期望的值达到了,那么就更新,否则,就不更新,CAS是CPU的并发原语!
System.out.println(atomicInteger.compareAndSet(2020, 2021)); //true
System.out.println(atomicInteger.get()); //2021 ,atomicInteger的值更新到了2021
System.out.println(atomicInteger.compareAndSet(2020, 2021)); //false
System.out.println(atomicInteger.get()); //2021,此时atomicInteger的值是2021,不更新
}
}
2.2.1 CAS实现原子操作的问题
CAS是并发包的基石,但用CAS有三个问题:
-
ABA问题
根源:CAS的本质是对变量的current value ,期望值 expected value 进行比较,二者相等时,再将 给定值 given update value 设为当前值。
因此会存在一种场景,变量值原来是A,变成了B,又变成了A,使用CAS检查时会发现值并未变化,实际上是变化了。
对于数值类型的变量,比如int,这种问题关系不大,但对于引用类型,则会产生很大影响。
ABA问题解决思路:版本号。在变量前加版本号,每次变量更新时将版本号加1,A -> B -> A,就变成 1A -> 2B -> 3A。
JDK5之后Atomic包中提供了AtomicStampedReference#compareAndSet来解决ABA问题。
public boolean compareAndSet(@Nullable V expectedReference,
V newReference,
int expectedStamp,
int newStamp)
-
循环时间长则开销大
自旋CAS若长时间不成功,会对CPU造成较大开销。不过有的JVM可支持CPU的pause指令的话,效率可有一定提升。
pause作用:
- 延迟流水线指令(de-pipeline),使CPU不至于消耗过多执行资源。
- 可避免退出循环时因内存顺序冲突(memorey order violation )引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
-
只能保证一个共享变量的原子操作
CAS只能对单个共享变量如是操作,对多个共享变量操作时则无法保证原子性,此时可以用锁。
另外,也可“取巧”,将多个共享变量合成一个共享变量来操作。比如a=2,b=t,合并起来ab=2t,然后用CAS操作ab。JDK5提供
保证引用对象间的原子性,它可将多个变量放在一个对象中来进行CAS操作。AtomicReference
三、CPU如何实现原子操作
3.1对于单核CPU
- 对于单核cpu,所有的事件都是串行,执行完第一才会去执行第二个。所以,单核CPU实现原子操作比较简单。
- 在单核CPU中,每个指令都保证是原子的,即中断只会在指令之间发生。Intel x86指令集支持内存操作数的inc操作,将多条指令的操作在一条指令内完成。因为进程的上下文切换是在总是在一条指令执行完成后,所以不会写撕裂或者读撕裂等并发问题。
3.2对于多核CPU
- 首先,CPU会自动保证基本的内存操作的原子性。CPU保证从内存中读写一个字节是原子的,即:当一个CPU读一个字节时,其他处理器不能访问这个字节的内存地址。
- 但对于复杂的内存操作如跨总线跨度、跨多个缓存行的访问,CPU是不能自动保证的。不过,CPU提供总线锁定和缓存锁定。
1、使用总线锁
- 总线锁用来锁住某一个共享内存。当一个cpu要对内存进行操作时,会加上总线锁,限制其他cpu对共享内存操作。Intel x86指令集提供了指令前缀lock用于锁定前端串行总线(FSB),保证了指令执行时不会受到其他处理器的干扰。
- 假如多个处理器同时读改写共享变量,这种操作(e.g. i++)不是原子的,操作完的共享变量的值会和期望的不一致。
- 原因:多个处理器同时从各自缓存读i,分别 + 1,分别写入内存。要想保证读改写共享变量的原子性,必须保证CPU1读改写该变量时,CPU2不能操作缓存了该变量内存地址的缓存。
- 总线锁就是解决此问题的。总线锁:利用LOCK#信号,当一个CPU在总线上输出此信号,其他CPU的请求会被阻塞,则该CPU可以独占共享内存。
- 使用总线锁,会锁定cpu与内存的通信,所以开销很大。有的cpu架构提供开销更小的缓存锁。缓存锁在一个cpu进行回写时,会使用缓存一致性机制来保护内部内存,当其他处理器回写已被锁定的缓存行的数据时会使缓存行无效。
- 同一时刻,其实只要保证对某个内存地址的操作是原子的即可,但总线锁定把CPU和内存间的通信锁住了。锁定期间,其他CPU不能操作其他内存地址的数据,所以总线锁定的开销比较大。目前CPU会在一些场景下使用缓存锁替代总线锁来优化。
- 频繁使用的内存会被缓存到L1、L2、L3高速cache中,原子操作可直接在高速cache中进行,不需要声明总线锁。
- 缓存锁是指:缓存一致性机制阻止同时修改由两个以上CPU缓存的内存区域数据,当其他CPU回写已被锁定的缓存行数据时,会使缓存行无效。
- CAS(Compare and Swap),cas记录原来内存中的值old,和将要修改的值new。CAS会检测现在内存中的值now,如果now和old一致,则说明没有别的cpu进行了内存修改,执行new值的更新。如果new和old值不等,则说明值已被修改,丢弃new值。