原子操作 与 Atomic原子类总结

2020/1/1

# 原子操作

# 基本概念

原子(atomic):不能被进一步分割的最小粒子。

原子操作(atomic operation):不可被中断的一个或一系列操作。

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

# 处理器如何实现原子操作?

如果多个处理器同时对共享变量进行读改写操作(i++就是经典的读改写操作),因为操作不是原子性的,所以可能出现共享变量的值会和期望的不一致的问题。

i++ 不是一个操作,而是三个操作:

  1. get取到 i的值
  2. 进行 +1 操作
  3. put写 i 的值

原因可能是:多个处理器同时从各自的缓存中读取变量i,分别进行加1操作,然后分别写入系统内存中

# 使用总线锁保证原子性

处理器可以使用 总线锁 保证原子性。基本思想如下:

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

# 使用缓存锁保证原子性

# 总线锁的缺陷

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

# 缓存锁基本思想

处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效

# 处理器不会使用缓存锁定的两种情况

  1. 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line)时,则处理器会调用总线锁定。
  2. 有些处理器不支持缓存锁定。

# Java如何实现原子操作?

在Java中可以通过 循环CAS 的方式来实现原子操作。

# 循环CAS 实现原子操作

# 什么是CAS?

JVM中的CAS操作正是利用了处理器提供的 CMPXCHG指令 实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

从Java 1.5开始,JDK的并发包里提供了一些类来支持原子操作,如AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更新的long值)。这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增1和自减1。

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

# 1. ABA问题

CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现 它的值没有发生变化,但是实际上却变化了

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么 A→B→A就会变成1A→2B→3A

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

public boolean compareAndSet(
    V          expectedReference,         // 预期引用
    V          newReference,              // 更新后的引用
    int         expectedStamp,            // 预期标志
    int         newStamp                  // 更新后的标志
)
1
2
3
4
5
6
# 2. 循环时间长开销大

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

如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令(depipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率

CAS自旋长时间,可以考虑使用失败策略。自旋达到一定的次数/时间,可以执行相应的处理——抛出异常/回滚。

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

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

两个办法:

  1. 使用锁
  2. 把多个共享变量合并成一个共享变量来操作。从Java 1.5开始,提供了 AtomicReference 类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

# 锁 实现原子操作

锁机制保证了 只有获得锁的线程才能够操作锁定的内存区域 ,可以保证原子性。

# concurrent包的实现原理

volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

  1. 声明共享变量为 volatile
  2. 使用CAS的原子条件更新来实现线程之间的同步。
  3. 配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:

concurrent包的实现示意图

参看:Atomic 原子类 (opens new window)