CAS 原理分析

CAS 原理分析

是什么?

CAS(Compare and Swap)即比较并替换。是一种在多线程环境下面实现同步功能的技术,保证了多线程执行的安全性

原理

CAS 比较替换,通俗来讲,比较当前内存地址valueOffset当中的值是否和旧的预期值是否一样,如果一样,就进行替换,如果不一样,则重来,重新获取valueOffset内存地址当中的值,继续上述步骤,直到替换成功

1
CAS(V,N,B)

V:代表内存地址,即valueOffset
N: 代表内存地址的值
B: 代表需要去替换的值

当线程1获取到V内存地址下的值为1,则旧的预期N的值,此时线程1挂起
现在线程2也获取到了V内存地址下面的值为1,此时内存地址N的值为1,去替换成B的值,改成2,线程2结束
线程1重新开始运行,此时, 线程1获取到内存地址的值为2,去和旧的预期值1相比较,不相等,则CAS失败重复上述步骤,重新更新旧的预期值为2,当没有线程操作V内存地址下的值时,此时内存地址下的值2等于旧的预期值2,即修改成功

这里得说一下volatitle,用volatitle修饰的value值,保证了值在工作内存中的可见性,所以,当value值被修改之后,后面的线程能看到值被变更,才能去比较

源码分析

这里以原子操作类 Atomic系列 为例

Atomic系列主要有三个类来操作三种不同的值

  • AtomicBoolean 操作bool值的原子类
  • AtomicInteger 操作Int值的原子类
  • AtomicLong 操作Long类型的原子类
    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
    30
    31
    32
    33
    /*
    这个是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。
    */
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //valueOffset是表示该变量在内存地址中的偏移地址,因为unsafe就是根据内存偏移地址获取数据
    private static final long valueOffset;

    static {
    try {
    //通过unsafe的objectFieldOffset方法获取value值在内存中的偏移地址,通过偏移量,unsafe可以去操作该内存的value值
    valueOffset = unsafe.objectFieldOffset
    (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
    }
    // value值使用了volatile 保证了value值在内存中的可见性
    private volatile int value;

    //设置一个值,并返回旧值
    public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

    // 利用底层的CAS操作去比较替换值
    public final int getAndSetInt(Object var1, long var2, int var4) {
    int var5;
    do {
    //获取到var2这个内存偏移量里面的值var5
    var5 = this.getIntVolatile(var1, var2);
    //然后下面的循环,会取到var2内存偏移量里面的值去和var5比较,如果相等将var4替换,如果不相等则继续循环
    } while(!this.compareAndSwapInt(var1, var2, var5, var4));
    //返回 var5 为旧值
    return var5;
    }

CAS缺陷

  最明显的缺陷就是ABA问题

当有三个线程 第一个线程将值改为1 挂起。第二个线程 将值改为2 结束。第三个线程又将值改为1 结束。此时第一个线程跑起来,去比较替换,发现内存中的值还是为1 此时替换成功。

这个看起来没有什么问题,但是在特殊场景下面,会出现麻烦。

比如:一个库存操作,库存100个 两个线程同时出库50个,此时第二次线程是误操作,应该只有第一次提取成功,将100元改成50.此时第二线程挂起,有人入库了50个,此时库存为100,则第二线程继续执行,发现库存还是100,则修改库存为50,此时库存为100,但是第一线程出库之后应该只有50,此时库存为100,则出现异常

  解决方案

  • AtomicStampedReference原子类是一个带有时间戳的对象引用,在每次修改后,AtomicStampedReference不仅会设置新值而且还会记录更改的时间。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值才能写入成功,这也就解决了反复读写时,无法预知值是否已被修改的窘境
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
//构造函数会包含一个时间戳去记录
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
//在设置CAS的操作过程中去调用casPair方法
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
//此时也是用的unsafe方法调用的compareAndSwapObject方法
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

总结

其实CAS就是无锁编程的一种实现,也是一个乐观锁,在任何情况下,都认为事情都是往好的一方面发展,当线程发现冲突时,则利用CAS去保证线程的安全性,这就是CAS实现无锁策略的核心所在

-------------本文结束感谢您的阅读-------------
0%