原子操作
先来介绍一下啥是原子操作,原子(atomic)本意是 “不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”
接下来,我们来看看实现原子操作的方式。
实现原子操作的方式
一、使用互斥(阻塞)同步
实现原子操作最容易想到的方式便是互斥同步了,通过使用编程语言提供给我们的同步锁,比如 Java 的关键字 synchronized 或者使用 J.U.C 包中的 ReentrantLock,保证了只有获得锁的线程才能够操作锁定的内存区域。因为只能有一个线程进入临界区,所以临界区内的一系列操作,必定是原子操作,不可被分割,即便进入临界区的线程耗尽 CPU 时间片,被挂起,其它线程也无法进入临界区。
注意:其实,Java 的 synchronized 从 JDK1.6 版本后,就不算严格意义上的互斥同步了,由于引入了各种锁优化,它也拥有了一些乐观锁的特点,感兴趣的可以看我另外两篇文章。
synchronized 锁优化(一):自适应自旋锁、锁消除、锁粗化
二、非阻塞同步:CAS
CAS(Compare and Swap,比较并交换)是 CPU 支持的原子性操作。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。自旋 CAS 实现的基本思路就是循环进行 CAS 操作直到成功为止。
在 Java 中除了使用同步锁,还可以使用循环 CAS 的方式来实现原子操作。比如 J.U.C 包中的原子操作类,都是使用 CAS 来实现原子操作的,它主要分为以下四类:
- 原子更新基本类型类:AtomicBoolean、AtomicInteger、AtomicLong
- 原子更新数组:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 原子更新引用类型:AtomicReference、AtomicMarkableReference、AtomicReferenceFieldUpdater
- 原子更新字段类:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference
虽然相较于互斥同步这种悲观锁,使用 CAS 这种乐观的并发策略,实现原子操作更为高效,但是 CAS 仍然存在如下三个问题。
- ABA 问题:因为 CAS 需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加 1,那么 A→B→A 就会变成 1A→2B→3A 。从 Java 1.5 开始,JDK 的 Atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题(就是我们上面刚介绍过的原子操作类中的原子更新引用类型)。这个类的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
- 循环时间长开销大:自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。
- 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量 i=2,j=a,合并一下 ij=2a,然后用 CAS 来操作 ij 。或者使用我们上面才介绍过的原子操作引用类型!将多个共享变量放入一个对象里来进行 CAS 操作!