原子操作的实现原理

原子(atomic)本意是”不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为”不可被中断的一个或者一系列操作”

术语定义

缓存行(Cache line);

缓存的最小操作单位

比较并交换(Compare and swap)

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

CPU流水线(CPU pipeline)

CPU流水线的工作方式就像工业生产上的装配流水线,在CPU中由56个不同功能的电路单元组成一条指令处理流水线,然后将一条x86指令分成56步再由这些电路单元分别执行,这样就能实现在一个CPU时钟周期完成一条指令,因此提高CPU的运算速度

内存顺序冲突(Memory order violation)

内存顺序冲突一般是由假共享引起的,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线

处理器如何实现原子操作

使用总线锁保证原子性

所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存

使用缓存锁来保证原子性

总同一时刻,我们只需要保证对某个内存地址的操作是原子性即可,但是总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大

频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁

所谓 缓存锁定 就是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当他执行锁操作回写到内存时,处理器不在总线上声言LOCK #信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性.

因为缓存一致性会阻止同时修改两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效

但是有两种情况下处理器不会使用缓存锁定:

  1. 操作的数据不能被缓存在处理器内部,操作的数据跨多个缓存行,处理器会调用总线锁定
  2. 有些处理器不支持缓存锁定

Java如何实现原子操作

在java中可以使用循环CAS实现原子操作

使用循环CAS实现操作

JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的.

自旋CAS实现的基本思路就是循环进行CAS操作直到成功

从Java1.5开始 JDK的并发包提供了一些类来支持原子操作

如 AtomicBoolean(原子更新boolean)

AtomicInteger(原子)

AtomicLong(原子)

CAS实现原子操作的三大问题

ABA问题

如果一个值由A变成B,再变成A,使用CAS检查的时候,会发现它的值没有发生变化,但是实际上是变化了.

ABA问题的解决思路就是带上版本号

1A->2B->3A

Java1.5 起,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题. 这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值.

循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销

只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,CAS操作无法保证原子性.这个时候要用锁

还有 一个取巧的办法就是把两个变量合并成一个共享变量来操作

使用锁机制实现原子操作

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域.JVM内部实现了很多锁机制,有偏向锁,轻量级锁和互斥锁.

有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS:

即 当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS解锁