这里写目录标题
- 一、概念
- 二、CAS 如何保证原子性
- 2.1、总线锁定
- 2.2、缓存锁定
- 二、底层原理
- 三、CAS典型应用
- 四、CAS问题
- 4.1、循环时间长,开销很大
- 4.2、只能保证一个共享变量的原子操作
- 4.3、引出来 ABA 问题
一、概念
判断内存中某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。映射到操作系统就是一条 cmpxchg
硬件汇编指令(保证原子性),其作用是让 CPU 将内存值更新为新值,但是有个条件,内存值必须与期望值相同,并且 CAS 操作无需用户态与内核态切换,直接在用户态对内存进行读写操作(意味着不会阻塞/线程上下文切换)
假如说有 3 个线程并发的要修改一个 AtomicInteger 的值,底层机制如下:
- 每个线程都会先获取当前的值,接着走一个原子的 CAS 操作。原子的意思就是这个 CAS 操作一定是自己完整执行完的,不会被别人打断;
- 然后 CAS 操作里,会比较一下,现在的值是不是刚才获取到的那个值。如果是,说明没人改过这个值,然后设置成累加 1 之后的一个值;
- 如果有人在执行 CAS 的时候,发现之前获取的值跟当前的值不一样,会导致 CAS 失败。失败之后,进入一个无限循环,再次获取值,接着执行 CAS 操作;
二、CAS 如何保证原子性
原子性是指一个或者多个操作在 CPU 执行的过程中不被中断的特性,要么执行,要不执行,不能执行到一半(不可被中断的一个或一系列操作)
为了保证 CAS 的原子性,CPU 提供了下面两种方式
- 总线锁定
- 缓存锁定
在多处理器环境下,LOCK# 信号可以确保处理器独占使用某些共享内存。LOCK 可以被添加在下面的指令前:
ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, CMPXCHG16B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG
通过在 inc 指令前添加 LOCK 前缀,即可让该指令具备原子性。多个核心同时执行同一条 inc 指令时,会以串行的方式进行,也就避免了上面所说的那种情况。那么这里还有一个问题,LOCK 前缀是怎样保证核心独占某片内存区域的呢?答案如下:
在 Intel 处理器中,有两种方式保证处理器的某个核心独占某片内存区域。第一种方式是通过锁定总线,让某个核心独占使用总线,但这样代价太大。总线被锁定后,其他核心就不能访问内存了,可能会导致其他核心短时内停止工作。第二种方式是锁定缓存,若某处内存数据被缓存在处理器缓存中。处理器发出的 LOCK# 信号不会锁定总线,而是锁定缓存行对应的内存区域。其他处理器在这片内存区域锁定期间,无法对这片内存区域进行相关操作。相对于锁定总线,锁定缓存的代价明显比较小
2.1、总线锁定
总线(BUS)是计算机组件间的传输数据方式,也就是说 CPU 与其他组件连接传输数据,就是靠总线完成的,比如 CPU 对内存的读写
总线锁定是指 CPU 使用了总线锁,所谓总线锁就是使用 CPU 提供的 LOCK# 信号,当 CPU 在总线上输出 LOCK# 信号时,其他 CPU 的总线请求将被阻塞
2.2、缓存锁定
总线锁定方式虽然保证了原子性,但是在锁定期间,会导致大量阻塞,增加系统的性能开销,所以现代 CPU 为了提升性能,通过锁定范围缩小的思想设计出了缓存行锁定(缓存行是 CPU 高速缓存存储的最小单位)。
所谓缓存锁定是指 CPU 对缓存行进行锁定,当缓存行中的共享变量回写到内存时,其他 CPU 会通过总线嗅探机制感知该共享变量是否发生变化,如果发生变化,让自己对应的共享变量缓存行失效,重新从内存读取最新的数据,缓存锁定是基于缓存一致性机制来实现的,因为缓存一致性机制会阻止两个以上 CPU 同时修改同一个共享变量(现代 CPU 基本都支持和使用缓存锁定机制)
二、底层原理
整体实现思路: 自旋(循环) + CAS算法
- 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B
- 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋
Unsafe 里的 CAS 操作相关
CAS 是一些 CPU 直接支持的指令,也就是我们前面分析的无锁操作,在 Java中 无锁操作 CAS 基于以下 3 个方法实现
// 第一个参数 o 为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
// expected 表示期望值,x 表示要设置的值,下面 3 个方法都通过 CAS 原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
并发包中的原子操作类(Atomic系列)
CAS 在 Java 中的应用,即并发包中的原子操作类(Atomic 系列),从JDK 1.5开始提供了 java.util.concurrent.atomic
包,在该包中提供了许多基于 CAS 实现的原子操作类,用法方便,性能高效,主要分以下 4 种类型
原子更新基本类型主要包括 3 个类:
- AtomicBoolean:原子更新布尔类型
- AtomicInteger:原子更新整型
- AtomicLong:原子更新长整型
原子更新引用数据类型主要包括:
- AtomicReference 原子操作引用对象类型
基本数据类型的原子操作类的实现原理和使用方式几乎是一样的,以 AtomicInteger 为例进行分析,AtomicInteger 主要是针对 int 类型的数据执行原子操作,它提供了原子自增方法、原子自减方法以及原子赋值方法等,源码如下
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 获取指针类Unsafe
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 下述变量 value 在 AtomicInteger 实例对象内的内存偏移量
private static final long valueOffset;
static {
try {
// 通过 unsafe 类的 objectFieldOffset() 方法,获取 value 变量在对象内存中的偏移
// 通过该偏移量 valueOffset,unsafe类的内部方法可以获取到变量 value 对其进行取值或赋值操作
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 当前 AtomicInteger 封装的 int 变量 value
private volatile int value;
public AtomicInteger() {
}
// 获取当前最新值
public final int get() {
return value;
}
// 设置当前值,具备 volatile 效果,方法用 final 修饰是为了更进一步的保证线程安全。
public final void set(int newValue) {
value = newValue;
}
// 最终会设置成 newValue,使用该方法后可能导致其他线程在之后的一小段时间内可以获取到旧值,有点类似于延迟加载
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
// 设置新值并获取旧值,底层调用的是 CAS 操作即 unsafe.compareAndSwapInt() 方法
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
// 如果当前值为 expect,则设置为update(当前值指的是value变量)
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
/*
this:当前对象
valueOffset:内存偏移量,根据内存偏移地址可以获取数据
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// 当前值减1,返回旧值,底层CAS操作
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
}
可以发现 AtomicInteger 原子类的内部几乎是基于前面分析过 Unsafe 类中的 CAS 相关操作的方法实现的,这也同时证明 AtomicInteger 是基于无锁实现的
我们发现 AtomicInteger 类中所有自增或自减的方法都间接调用 Unsafe 类中的 getAndAddInt() 方法实现了 CAS 操作,从而保证了线程安全,关于 getAndAddInt 其实前面已分析过,它是 Unsafe 类中 1.8 新增的方法,源码如下
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
可看出 getAndAddInt 通过一个 while 循环不断的重试更新要设置的值,直到成功为止,调用的是 Unsafe 类中的 compareAndSwapInt 方法,是一个 CAS 操作方法。这里需要注意的是,上述源码分析是基于JDK1.8 的,如果是 1.8 之前的方法,AtomicInteger 源码实现有所不同,是基于 for 死循环的,如下:
// JDK 1.7的源码,由 for 的死循环实现,并且直接在 AtomicInteger 实现该方法,
// JDK1.8 后,该方法实现已移动到 Unsafe 类中,直接调用 getAndAddInt 方法即可
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
AtomicReference 类实现自旋锁
自旋锁是假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这种方式确实也是可以提升效率的。但问题是当线程越来越多竞争很激烈时,占用 CPU 的时间变长会导致性能急剧下降,因此 Java 虚拟机内部一般对于自旋锁有一定的次数限制,可能是 50 或者 100 次循环后就放弃,直接挂起线程,让出 CPU 资源
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
//使用 CAS 原子操作作为底层实现
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t come in");
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void myUnLock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t invoked myUnLock()");
}
public static void main(String[] args) throws InterruptedException {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(()-> {
try {
spinLockDemo.myLock();
TimeUnit.SECONDS.sleep(5);
spinLockDemo.myUnLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "AA").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()-> {
spinLockDemo.myLock();
spinLockDemo.myUnLock();
}, "BB").start();
}
}
/*
AA come in
BB come in
AA invoked myUnLock()
BB invoked myUnLock()
*/
原子类更新引用
class User {
User(String name, int age)
{
this.name = name;
this.age = age;
}
private String name;
private int age;
}
public class Solution {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) throws InterruptedException {
AtomicReference<User> atomicReference = new AtomicReference<>();
User user = new User("monster", 18);
User updateUser = new User("jack", 25);
atomicReference.set(user);
System.out.println(atomicReference.get());
atomicReference.compareAndSet(user, updateUser);
System.out.println(atomicReference.get());
}
}
/*
User@74a14482
User@1540e19d
*/
三、CAS典型应用
java.util.concurrent.atomic
包下的类大多是使用 CAS 操作来实现的,比如 AtomicInteger、AtomicLong。一般在竞争不是特别激烈的时候,使用该包下的原子操作性能比使用 synchronized 关键字的方式高效的多;
在较多的场景都可能会使用到这些原子类操作。一个典型应用就是计数了,在多线程的情况下需要考虑线程安全问题
public class Increment {
private int count = 0;
public void add() {
count++;
}
}
在并发环境下对 count 进行自增运算是不安全的,因为 count++ 不是原子操作,而是三个原子操作的组合:
- 读取内存中的 count 值赋值给局部变量 temp;
- 执行 temp + 1 操作;
- 将 temp 赋值给 count;
所以如果两个线程同时执行 count++ 的话,不能保证线程一按顺序执行完上述三步后线程二才开始执行:
解决方案
1、Synchronized 加锁。同一时间只有一个线程能加锁,其他线程需要等待锁,这样就不会出现 count 计数不准确的问题了
public class Increment {
private int count = 0;
public synchronized void add() {
count++;
}
}
但是引入 Synchronized 会造成多个线程排队的问题,相当于让各个线程串行化了,一个接一个的排队、加锁、处理数据、释放锁,下一个再进来。同一时间只有一个线程执行,这样的锁有点重量级了
这类似于悲观锁的实现,需要获取这个资源,就给它加锁,别的线程都无法访问该资源,直到操作完后释放对该资源的锁。虽然随着 Java 版本更新,也对 Synchronized 做了很多优化,但是处理这种简单的累加操作,仍然显得"太重了"
2、Atomic 原子类。对于 count++ 的操作,完全可以换一种做法,Java 并发包下面提供了一系列的 Atomic 原子类,比如说 AtomicInteger
public static void main(String[] args) {
public static AtomicInteger count = new AtomicInteger(0);
public static void increase() {
count.incrementAndGet();
}
}
多个线程可以并发的执行 AtomicInteger 的 incrementAndGet(),意思就是把 count 的值累加 1,接着返回累加后最新的值。实际上,Atomic 原子类底层用的不是传统意义的锁机制,而是无锁化的 CAS 机制,通过 CAS 机制保证多线程修改一个数值的安全性
四、CAS问题
4.1、循环时间长,开销很大
自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率
4.2、只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量 i=2,j=a,合并一下 ij = 2a
,然后用 CAS 来操作 ij。从 Java1.5 开始JDK提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作
4.3、引出来 ABA 问题
因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A -> B -> A 就会变成 1A -> 2B -> 3A