CAS的全称为Compare And Swap,是unsafe的一个方法。主要是为了保证在多线程下保证多变量修改的原子性操作。CAS执行的是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在intel的CPU中,使用的是cmpxchg
指令,就是说CAS是靠硬件实现的,从而在硬件层面提升效率。如果在多核CPU的环境下,在CAS的底层实现将会增加一个lock指令来对缓存和总线进行加锁的操作,从而保证CAS的操作原子性。主要的应用场景是JUC的Atomic的原子实现例如:AtomicInteger和AtomicLong类。还有就是的实现多线程对共享资源的竞争的互斥性操作,例如:AQS,CurrentHashMap,ConcurrntlinkQueue类。
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,其它原子操作都是利用类似的特性完成的。 在 java.util.concurrent
下面的源码中,Atomic
, ReentrantLock
都使用了Unsafe类中的方法来保证并发的安全性。
CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁,JDK中大量使用了CAS来更新数据而防止加锁来保持原子更新。CAS 操作包含三个操作数 :内存偏移量位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
// 使用 unsafe 类的原子操作方式
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
//计算变量 value 在类对象中的偏移量
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
Unsafe
调用C 语言可以通过偏移量对变量进行操作
//volatile变量value
private volatile int value;
/**
* 创建具有给定初始值的新 AtomicInteger
*
* @param initialValue 初始值
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
//返回当前的值
public final int get() {
return value;
}
//原子更新为新值并返回旧值
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
//最终会设置成新值
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
//如果输入的值等于预期值,则以原子方式更新为新值
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//方法相当于原子性的 ++i
public final int getAndIncrement() {
//三个参数,1、当前的实例 2、value实例变量的偏移量 3、递增的值。
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//方法相当于原子性的 --i
public final int getAndDecrement() {
//三个参数,1、当前的实例 2、value实例变量的偏移量 3、递减的值。
return unsafe.getAndAddInt(this, valueOffset, -1);
}
二、Unsafe 源码解析
在JDK8中追踪可见sun.misc.Unsafe
这个类是无法看见源码的,打开openjdk8
源码看 目录:openjdk\jdk\src\share\classes\sun\misc\Unsafe.java
通常我们最好也不要使用Unsafe
类,除非有明确的目的,并且也要对它有深入的了解才行。要想使用Unsafe
类需要用一些比较tricky
的办法。Unsafe类使用了单例模式,需要通过一个静态方法getUnsafe()
来获取。但Unsafe类做了限制,如果是普通的调用的话,它会抛出一个SecurityException
异常;只有由主类加载器加载的类才能调用这个方法。
//获取Unsafe实例静态方法
@CallerSensitive
public static Unsafe getUnsafe() {
Class caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
//native硬件级别的原子操作
//类似的有compareAndSwapInt,compareAndSwapLong,compareAndSwapBoolean,compareAndSwapChar等等。
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
//内部使用自旋的方式进行CAS更新(while循环进行CAS更新,如果更新失败,则循环再次重试)
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//获取对象内存地址偏移量上的数值v
v = getIntVolatile(o, offset);
//如果现在还是v,设置为 v + delta,否则返回false,继续循环再次重试.
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
利用 Unsafe 类的 JNI compareAndSwapInt 方法实现,使用CAS实现一个原子操作更新,compareAndSwapInt 四个参数:1、当前的实例 2、实例变量的内存地址偏移量 3、预期的旧值 4、要更新的值。
到这里 CAS 的实现过程就讲了,CAS的实现离不开处理器的支持。可以发现AtomicInteger
原子类的内部几乎是基于前面分析过Unsafe
类中的CAS
相关操作的方法实现的,这也同时证明AtomicInteger
getAndIncrement
自增操作实现过程,是基于无锁实现的。
假设这样一种场景,当第一个线程执行CAS(V,E,U)操作。在获取到当前变量V,准备修改为新值U前,另外两个线程已连续修改了两次变量V的值,使得该值又恢复为旧值,这样的话,我们就无法正确判断这个变量是否已被修改过,如下图:
这就是典型的ABA问题,一般情况这种情况发现的概率比较小,可能发生了也不会造成什么问题,比如说我们对某个做加减法,不关心数字的过程,那么发生ABA问题也没啥关系。但是在某些情况下还是需要防止的,那么该如何解决呢?在Java中解决ABA问题,我们可以使用以下原子类
3.1 AtomicStampedReference类AtomicStampedReference原子类是一个带有时间戳的对象引用,在每次修改后,AtomicStampedReference不仅会设置新值而且还会记录更改的时间。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值才能写入成功,这也就解决了反复读写时,无法预知值是否已被修改的窘境。底层实现为: 通过Pair私有内部类存储数据和时间戳, 并构造volatile修饰的私有实例。接着看 java.util.concurrent.atomic.AtomicStampedReference
类的compareAndSet()方法的实现:
private static class Pair {
final T reference;
final int stamp;
//最好不要重复的一个数据,决定数据是否能设置成功,时间戳会重复
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static Pair of(T reference, int stamp) {
return new Pair(reference, stamp);
}
}
同时对当前数据和当前时间进行比较,只有两者都相等是才会执行casPair()方法,单从该方法的名称就可知是一个CAS方法,最终调用的还是Unsafe
类中的compareAndSwapObject
方法到这我们就很清晰AtomicStampedReference
的内部实现思想了,通过一个键值对Pair
存储数据和时间戳,在更新时对数据和时间戳进行比较,只有两者都符合预期才会调用Unsafe
的compareAndSwapObject
方法执行数值和时间戳替换,也就避免了ABA的问题。
/**
* 原子更新带有版本号的引用类型。
* 该类将整数值与引用关联起来,可用于原子的更数据和数据的版本号。
* 可以解决使用CAS进行原子更新时,可能出现的ABA问题。
*/
public class AtomicStampedReference {
//静态内部类Pair将对应的引用类型和版本号stamp作为它的成员
private static class Pair {
//最好不要重复的一个数据,决定数据是否能设置成功,建议时间戳
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
//根据reference和stamp来生成一个Pair的实例
static Pair of(T reference, int stamp) {
return new Pair(reference, stamp);
}
}
//作为一个整体的pair变量被volatile修饰
private volatile Pair pair;
//构造方法,参数分别是初始引用变量的值和初始版本号
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
....
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
private static final long pairOffset = objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
//获取pair成员的偏移地址
static long objectFieldOffset(sun.misc.Unsafe UNSAFE,
String field, Class klazz) {
try {
return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));
} catch (NoSuchFieldException e) {
NoSuchFieldError error = new NoSuchFieldError(field);
error.initCause(e);
throw error;
}
}
}
/**
* @param 期望(老的)引用
* @param (新的)引用数据
* @param 期望(老的)标志stamp(时间戳)值
* @param (新的)标志stamp(时间戳)值
* @return 是否成功
*/
public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) {
Pair current = pair;
return
// 期望(老的)引用 == 当前引用
expectedReference == current.reference &&
// 期望(老的)标志stamp(时间戳)值 == 当前标志stamp(时间戳)值
expectedStamp == current.stamp &&
// (新的)引用数据 == 当前引用数据 并且 (新的)标志stamp(时间戳)值 ==当前标志stamp(时间戳)值
((newReference == current.reference && newStamp == current.stamp) ||
#原子更新值
casPair(current, Pair.of(newReference, newStamp)));
}
//当引用类型的值与期望的一致的时候,原子的更改版本号为新的值。该方法只修改版本号,不修改引用变量的值,成功返回true
public boolean attemptStamp(V expectedReference, int newStamp) {
Pair current = pair;
return
expectedReference == current.reference &&
(newStamp == current.stamp ||
casPair(current, Pair.of(expectedReference, newStamp)));
}
/**
* CAS真正实现方法
*/
private boolean casPair(Pair cmp, Pair val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
期望 Pair cmp(A) == 当前内存存偏移量位置 Pair(V),就更新值 Pair val(B)成功返回true 否则 false。
public static void main(String[] args) {
AtomicStampedReference num = new AtomicStampedReference(1, 0);
Integer i = num.getReference();
int stamped = num.getStamp();
if (num.compareAndSet(i, i + 1, stamped, stamped + 1)) {
System.out.println("测试成功");
}
}
通过以上原子更新方法,可见 AtomicStampedReference就是利用了Unsafe的CAS方法+Volatile关键字对存储实际的引用变量和int的版本号的Pair实例进行更新。
博文参考 《JDK源码分析》