Java 并发编程—CAS 机制
CAS 机制
Java 并发编程-CAS 机制什么 CAS ?
在 Java 中,锁分为两类,一种是悲观锁 Synchronized
,一种是乐观锁 CAS 机制
。
CAS 机制是 Compare And Swap
的缩写,比较替换的操作。是用于在多线程下提供原子性操作
。
CAS有三个操作数:
内存值V
、旧的预期值A
、要修改的值B
,当且仅当预期值 A 与内存值 V 相等时,则将内存值修改为 B 并返回true,否则返回 false。
不理解不要紧,下面通过 JDK 提供的实现,来看 CAS 是如何实现原子性操作的。
JDK 提供的原子操作类
在 JDK 包 java.util.concurrent.atomic
提供很多原子性操作的类。
- 基于基本数据
AtmoicInteger,AtomicLong,AtmoicBoolean...
- 基于数组
AtomicIntegerArray...
- 基于引用
AtomicReference...
- 基于更新字段
AtomicIntegerFieldUpdater...
atomic下面基于 JDK 提供 CAS 机制 AtmoicInteger
来修改上面的程序
public class AtomicPersonCount implements Runnable {
//①
private AtomicInteger personCount = new AtomicInteger(0);
public static void main(String[] args) {
AtomicPersonCount waiter = new AtomicPersonCount();
Thread person1 = new Thread(waiter);
Thread person2 = new Thread(waiter);
person1.start();
person2.start();
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//②
personCount.incrementAndGet();
}
System.out.println(Thread.currentThread().getName() + "-人数:" + personCount.get());
}
}
在上面的程序中主要修改的点有两处,①将变量 personCount 使用 AtomicInteger 来包装,②处使用 personCount.incrementAndGet() 进行累加操作。这样,两个线程进行累加的就一定是200了。
AtomicInteger 的工作原理
上面改造的代码中
personCount++
操作替换为incrementAndGet
,获取线程工作内存的值 A,然后进行累加计算得到 B,然后将 A 和主存 V 进行比较,只有 A 和 V 的值一样的情况,才会将主存中的值 V 更新为 B,否则就不断的循环重试,直到成功,这个过程就自旋CAS。
基于上面这段代码,我们来分析一下 AtomicInteger
的是如何实现原子性操作的?
AtomicInteger 初始化做了什么事?
//AtomicInteger.java
//①
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;
在 ① 中得到 Unasfe
类对象,观察Unasfe
源码,它内部定义了很多 native 方法,AtomicInteger 内部的 CAS
机制就是通过 Unsafe
类来实现的。
②valueOffset
是通过 Unsafe
的 objectFieldOffset
方法获取的,它的作用是可以获取AtomicInteger
成员属性 value
在内存中地址相对于对象内存地址的偏移量
。简单理解就是 value
这个成员变量在内存中的地址
,拿到这个内存地址偏移值之后,后续就可以直接从内存地址位置进行读取。
在③中定义的 value
属性比较好理解,它就是当前 AtomicInteger
保存的值。
内部CAS是如何进行的?
我们通过调用 increamentAndGet()
方法即可给我们的值进行累加
。通过观察源码,内部是使用 unsafe.getAndAddInt(...)
实现 CAS 的。
- AtomicInteger.incrementAndGet
//AtomicInteger.java
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
//内部会进行累加操作,然后返回累加后的值
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
- UnSafe.getAndAddInt
//Unsafe.java
//var1表示当前 AtomicInteger 对象
//var2 表示当前 value 在内存的偏移值
//var4 表示当前需要累加的值
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//得到当前线程工作内存的值
var5 = this.getIntVolatile(var1, var2);
//将拿到 var5 的值通过 `compareAndSwapInt(...)` 与主存中的值进行比对,如果相等,将内存值替换为 `var5+var4` 并返回 true,表示替换成功。如果不想等,也就是主存中的值被其他线程修改了,那么返回 false,继续循环,再次从主存中获取最新的值,然后赋值给 var5 ,然后再进一次比对,直到成功为止。这个过程就是上面说到的 `CAS自旋`。
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
//③返回累加前值var5。
return var5;
}
CAS 存在的缺陷
- ABA问题
假如一个值原来是A,变成了B,又变成了A,那么CAS检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
AtomicStampedReference中的compareAndSet方法声明如下:
public boolean compareAndSet(
V expectedReference, // 预期引用
V newReference, // 更新后的引用
int expectedStamp, // 预期标志
int newStamp // 更新后的标志
)
举个栗子:老张(线程A)
来到客厅,发现有点口渴
,然后拿一个水杯(对象one)
倒了一杯水,这时突然想去上个 WC
,因此放下手中的水杯(对象one)
。这时老王(线程B)
来了,他刚好也口渴,看到桌上有一杯水
,然后就喝下去了,之后拿同一个水杯(对象 one)
再去倒一杯水,又放回同样的位置
。当老张(线程A)
去完 WC 后(抢到 CPU 执行权地线程),发现桌上那杯水还在,那他也把这杯水喝了。
上面这个栗子就是 ABA
问题,老张的水中途是被隔壁老王喝了,但是他去完 WC 之后,发现杯子还是那个杯子(还是对象one),这时就错误地认为数据中途没有被改动。
在 JDK1.5 中提供了 AtomicStampedReference
类,来解决 ABA 问题,下面我们来看看这个类是如何使用的?
public class AtomicStampedReferenceABA {
private static Object one = new Object();
public static void main(String[] args) {
//解决 ABA 问题
final AtomicStampedReference<Object> ai = new AtomicStampedReference<>(one, 0);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//老王来了,看到一杯水,喝掉它
boolean result = ai.compareAndSet(one, one, 0, 1);
System.out.println("老王 CAS 是否成功?" + result + " stamp = " + ai.getStamp());
}
});
t1.start();
try {
//老张去 WC
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//老张 WC 回来了,这时发现我的水杯别人喝过,这时我们失败
boolean result = ai.compareAndSet(one, one, 0, 1);
System.out.println("老张CAS是否成功?" + result + " stamp = " + ai.getStamp());
try {
t1.join(1000);
System.out.println("stamp = " + ai.getStamp());
System.out.println("对象比较 " + (one == ai.getReference()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
老王 CAS 是否成功?true stamp = 1
老张CAS是否成功?false stamp = 1
stamp = 1
对象比较 true
这里老张的水被老王喝了,所以他从 WC 回来之后,就不能喝了,也就是 compareAndSet 会失败。这样就解决了 ABA 问题了。
-
在 incrementAndGet() 中可以看出,如果自旋时间过程长,就会给 CPU 带来非常大的执行开销。
-
Atomic 原子性操作只能保证一个共享变量的原子性。
总结
本文总结 CAS 机制的原理,以及通过源码的角度来分析内部的实现,然后分析了 CAS 缺陷,并引入了一个隔壁老王喝老张水的栗子来说明 ABA 问题,并通过 AtomicStampedReference 来解决 ABA 问题。
参考
Java 多线程编程核心技术
Java并发编程之CAS原理分析
AtomicInteger 源码解析
AtomicStampedReference解决ABA问题
记录于 2019年3月30号