Compare and Swap - tenji/ks GitHub Wiki
CAS 基本实现原理
了解 CAS,首先要清楚 JUC,那么什么是 JUC 呢?JUC 就是java.util.concurrent
包的简称。它有核心就是 CAS 与 AQS 。CAS 是java.util.concurrent.atomic
包的基础,如 AtomicInteger、AtomicBoolean、AtomicLong 等等类都是基于 CAS。
什么是 CAS 呢?全称 Compare And Swap,比较并交换。CAS 有三个操作数,内存值 V,旧的预期值 E,要修改的新值 N。当且仅当预期值 E 和内存值 V 相同时,将内存值 V 修改为 N,否则什么都不做。
样例解析
如果我们需要对一个数进行加法操作,应该怎样去实现呢?我们模拟多个线程情况下进行操作。
ThreadDemo.java 实现一个 Runnable 接口:
package com.spring.security.test;
public class ThreadDemo implements Runnable {
private int count = 0;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
addCount();
}
}
private void addCount() {
count++;
}
public int getCount() {
return count;
}
}
ThreadTest.java 创建线程池,提交 10 个线程执行,预期结果应该是 1000。
package com.spring.security.test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadTest {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(10);
ThreadDemo threadDemo = new ThreadDemo();
for (int i = 0; i < 10; i++) {
threadPool.submit(threadDemo);
}
threadPool.shutdown();
System.out.println(threadDemo.getCount());
}
}
运行结果:874 或其他,与预期结果不符合。
执行出来的结果并不是想象中的结果。这是为什么呢?这跟线程的执行过程有关。
所以我们需要在改变 count,将值从高速缓冲区刷新到主内存后,让其他线程重新读取主内存中的值到自己的工作内存。
此时可以用volatile关键字。它的作用是保证对象在内存中的可见性。
修改 ThreadDemo 中的 count 字段。
private volatile int count = 0;
此时执行结果:900 或其他,与预期结果任然不符合。
此时还是并未得出正确执行结果。为什么?听我细细道来。
线程安全主要体现在三个方面:
- 原子性:提供了互斥访问,同一时刻只能有一个线程对它进行操作
- 可见性:一个线程对主内存的修改可以及时的被其他线程观察到
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序
目前可见性已经实现了,缺少原子性的操作,因为同一时刻,多个线程对其操作,会将改动后的最新值读取到自己的工作内存进行操作,最终只能得到后一个执行线程操作的结果,所以相当于少了一步操作,就会造成数据的不一致。
此时可以使用 JUC 的 Atomic 包下面的类来进行操作。
Atomic 类是使用 CAS+volatile 来实现原子性与可见性的。
我们来改造一下TheadDemo.java中的实现方法:
package com.spring.security.test;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadDemo implements Runnable {
// 使用 AtomicInteger 替代 int
private AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 递增
count.getAndIncrement();
}
}
public int getCount() {
return count.get();
}
}
执行结果: 1000,符合预期值。
接下来我们来分析一下 AtomicInteger 类的源码:
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
Unsafe 类是不安全的类,它提供了一些底层的方法,我们是不能使用这个类的。AtomicInteger 的值保存在 value 中,而 valueOffset 是 value 在内存中的偏移量,利用静态代码块使其类一加载的时候就赋值。value 值使用 volatile,保证其可见性。
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
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;
}
var1 表示当前对象,var2 表示 value 在内存中的偏移量,var4 为增加的值。var5 为调用底层方法获取 value 的值。
compareAndSwapInt 方法通过 var1 和 var2 获取当前内存中的 value 值,并与 var5 进行比对,如果一致,就将 var5 + var4 的值赋给 value,并返回 true,否则返回 false。
由 do while 语句可知,如果这次没有设置进去值,就重复执行此过程。这一过程称为自旋。
compareAndSwapInt 是 JNI (Java Native Interface) 提供的方法,可以是其他语言写的。
与 synchronized 比较
使用 synchronized 进行加法:
package com.spring.security.test;
public class ThreadDemo implements Runnable {
private int count = 0;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 递增
synchronized (ThreadDemo.class) {
count++;
}
}
}
public int getCount() {
return count;
}
}
使用 synchronized 和 AtomicInteger 都能得到预期结果,但是他们之间各有什么劣势呢?
synchronized 是重量级锁,是悲观锁,就是无论你线程之间发不发生竞争关系,它都认为会发生竞争,从而每次执行都会加锁。
在并发量大的情况下,如果锁的时间较长,那将会严重影响系统性能。
CAS 操作中我们可以看到 getAndAddInt 方法的自旋操作,如果长时间自旋,那么肯定会对系统造成压力。而且如果 value 值从 A -> B -> A,那么 CAS 就会认为这个值没有被操作过,这个称为 CAS 操作的ABA
问题。