0%

CAS操作

CAS操作

之前说在java.util.concurrent.atomic包下提供的原子操作类底层使用的是CAS,那么什么是CAS呢,CAS的全称为Compare And Swap,比较并替换,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B

在进行变量更新时,需要对比预期值A和内存地址V中的实际值,如果两者相同,才会将V对应的的值改为B

AtomicInteger为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// unsafe提供了硬件级别的原子操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 存储value在AtomicInteger中的偏移量
private static final long valueOffset;
// 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的
private volatile int value;

static {
try {
// valueOffset表示的是AtomicInteger对象value成员变量在内存中的偏移量,可以当做是value变量的内存地址
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

public final boolean compareAndSet(int expect, int update) {
// this为当前对象,valueOffset参数代表了V对象中的变量的偏移量,expect参数代表了A变量预期值,update参数代表了B新的值
// 如果对象this中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

Unsafe类

CAS的底层使用的是Unsafe类,Unsafe类中的方法都是native方法,看下提供了哪些方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该Unsafe函数中指定字段时使用
public native long objectFieldOffset(Field field);
// 获取数组中第一个元素的地址
public native int arrayBaseOffset(Class<?> arrayClass);
// 获取数组中一个元素占用的字节
public native int arrayIndexScale(Class<?> arrayClass);
// 比较对象obj中偏移量为offset的变量值是否与except相等,相等则使用update替换旧的except
public final native boolean compareAndSwapInt(Object obj, long offset, int except, int update);
// 获取obj对象中偏移量为offset的变量对应volatile语义的值
public native Object getObjectVolatile(Object obj, long offset);
// 设置obj对象中offset偏移的类型为Object的field的值为value
public native void putObjectVolatile(Object obj, long offset, Object value);
// 阻塞当前线程,isAbsolute等于false且time等于0表示一直阻塞;time大于0表示等待指定的time后阻塞线程会被唤醒,这个time为所休眠的时间;如果isAbsolute等于true,且time大于0,表示阻塞的线程到指定的时间点后会被唤醒,这个time为毫秒级的时间戳
// 如果其他线程调用了当前阻塞线程的interrupt方法而中断了当前线程时,或者其他线程调用了unPark方法并且把当前线程作为参数时,当前线程都会返回
public native void park(boolean isAbsolute, long time);
// 唤醒调用park后阻塞的线程
public native void unpark(Object thread);

Unsafe类不可以直接使用,因为Unsafe会判断当前的类加载器是不是Bootstrap类加载器,如果不是的话,会抛出异常SecurityException

1
2
3
4
5
6
7
8
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}

既然不能直接使用,那么只能使用反射来使用Unsafe类了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class TestUnsafe {
static final Unsafe unsafe;

static final long valueOffset;

private volatile int value = 0;

static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);

unsafe = (Unsafe) theUnsafe.get(null);

valueOffset = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("value"));
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
throw new RuntimeException("");
}

}

public static void main(String[] args) {
TestUnsafe testUnsafe = new TestUnsafe();

unsafe.getAndAddInt(testUnsafe, valueOffset, 1);
System.out.println(testUnsafe.value);
}
}

CAS的缺陷

虽然CAS采用的无锁操作来提供性能,但是CAS并不是完美的,存在了很多的不足

  • CPU开销大,在高并发的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,一直循环下去,会给CPU带来很大的压力
  • 不能保证代码块的原子性,CAS只能保证一个变量的原子性操作,而不能保证整个代码块的原子性
  • ABA问题,由于CAS对比的是两个最终值,所以可能会导致中间过程中值变化无法感知,变量的值从A变成B,然后再从B变成A,构成了环形转换

ABA问题的解决

解决这个问题很简单,ABA的本质就是无法感知中间过程,那么加一个版本号就可以了,每次版本号递增1,在比较时,不仅要比较内存地址V和旧的预期值A,还要比较一下版本号

在JDK中AtomicStampedReference类给每个变量都配备了一个时间戳stamp,从而避免了ABA问题的产生

1
2
3
4
5
int stamp = 1;
// 使用stamp来作为初始时间戳,为数据增加一个版本号,在修改数据的时候除了提供旧值之外,还要提供旧的版本号
AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(0,stamp);
// 预期值,更新后的值,预期版本,更新后的版本
stampedReference.compareAndSet(0,1,stamp,stamp+1);

欢迎关注我的其它发布渠道