JAVA 中的 CAS 和 ABA

学习多线程的时候看到很多人说CAS,并且在concurrent包里面到处都是CAS,就稍微去学习了一下。稍微做个总结

简介:

CAS: Compare and Swap
直接翻译就是 比较并交换。
直接从字面上理解就会发现我们已经有了疑问:

  • 比较,和什么比较,或者谁和谁比较。
  • 交换,和什么交换,或者谁和谁交换。

Compare and Swap 简析

先找一个使用到这个算法的类入手来学习,这里选择AtomicInteger类,找到其中一个比较常见的方法incrementAndGet来进行分析,下面先给出调用的方法,

    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }


    //Unsafe 类中的方法  Unsafe类中几乎都是native的方法。
    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;
    }

    //最终的调用方法   
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

    //与之类似的还有  compareAndSwapLong   compareAndSwapObject

incrementAndGet作用时是将当前的值 +1 并返回结果。可以看到其实CAS操作在这个是对值进行修改的,那么这个比较应该就是在值之间进行比较了。再分析方法之前我们还需要关注一下看AtomicInteger类中的一些默认的参数和构造函数,这样做便于追踪最终调用的方法compareAndSwapInt中的各个参数。

 // setup to use Unsafe.compareAndSwapInt for updates
    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;

    /**
     * Creates a new AtomicInteger with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    /**
     * Creates a new AtomicInteger with initial value {@code 0}.
     */
    public AtomicInteger() {
    }

几个细节需要注意:

  1. value是由volatile修饰的,volatile的作用时保证变量内存可见性,也就是内存中其他线程可以读写这个参数。在这里的incrementAndGet方法操作的就是这个变量。
  2. valueOffset:这里指的就是value这个属性在内存中的偏移量(内存中的地址,而不是值),当类被加载时先按顺序初始化static变量和static块,通过unsafe中的public native long objectFieldOffset(Field var1);获取AtomicInteger类属性value在内存中的偏移量,并将偏移量值赋给valueOffset。所以这个属性的值是value属性在内存中的偏移量。

对于incrementAndGet方法中逻辑是先调用的getAndAddInt方法,在其最终的结果 + 1 之后返回,也就是说实际上getAndAddInt应该是对内存进行了操作,但是返回值本身并不是操作之后的结果。继续看getAndAddInt参数从字面上直接理解,第一个参数是this 也就是当前的对象本身,第二个参数是偏移量,第三个为固定数值 1 。 getAndAddInt 源码中先调用了getIntVolatile方法,这方法,我源码中没有注释,在别的地方找到了一个注释:

 /***
   * Retrieves the value of the integer field at the specified offset in the
   * supplied object with volatile load semantics.
   *
   * @param obj the object containing the field to read.
   * @param offset the offset of the integer field within <code>obj</code>.
   */

  public native int getIntVolatile(Object obj, long offset);

大概的意思就是获取obj对象中offset偏移地址对应的整型field的值,并且支持volatile load语义。volatile的使用保证了即使此时数据被修改,也是对当前线程可见的。整个方法简单理解就是取offset所能确定的整型值。
取到值之后,进入后续的关键调用compareAndSwapInt,继续找注释。

/***
   * Compares the value of the integer field at the specified offset
   * in the supplied object with the given expected value, and updates
   * it if they match.  The operation of this method should be atomic,
   * thus providing an uninterruptible way of updating an integer field.
   *
   * @param obj the object containing the field to modify.
   * @param offset the offset of the integer field within <code>obj</code>.
   * @param expect the expected value of the field.
   * @param update the new value of the field if it equals <code>expect</code>.
   * @return true if the field was changed.
   */

  public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);

这个方法其实字面意思就能理解,expect就是预期的值,update就是要更新的值。
解读注释:比较从obj对象指定偏移量的地方读出来的整型数与传入的预期值进行对比,假如两者相匹配,则将对应地址的值更新为传入的update的值。这个操作必须具有原子性,所以这里提供了一个不能被打断的方法。
这里带入数据就是obj为this即当前对象,offset为指定的偏移量,expect是从offset偏移量中取出的整型数,update为取出来的数+1之后的结果。
看到这里就很清晰了,原先的问题谁和谁比,就是对象的指定偏移量中取出的指定类型的值与传入的预期值进行比较。而交换也很明确可以看出来是原有的值和需要更新的值进行交换。

  • compareAndSwapInt 为什么需要原子性
    其实很容易分析出来,假如方法运行到了取值出来并且比较通过了准备将新的数据写进去的时候了,此时又有另一个线程对同一个偏移量的值进行修改,并且也通过了比较的操作。这个时候两次修改,先修改的必将被后修改的覆盖,最终造成的结果可能就是值只是增加了1 而预期的应该增加2 所以这里必须是原子操作,

  • ABA 问题
    假设有两个线程T1、T2,同时操作队形O:

    • T1读取数据得到A,
    • 此时T1被挂起,T2执行。
    • T2 读取数据也是A,并且执行,将其修改为B
    • T2 继续操作,在此以同样的操作将B又改为A
    • 此时T2被挂起,T1继续执行
    • T1 获取到数据此时还是A(修改过之后)
    • T1 继续将数据修改。

    虽然从结果上看,并没有问题但是从过程上看并不是预期的,并且是存在安全隐患的。
    ABA问题我们可以使用JDK的并发包中的AtomicStampedReferenceAtomicMarkableReference来解决, AtomicStampedReferenceAtomicMarkableReference是通过版本号(时间戳)来解决ABA问题的,我们也可以使用版本号(verison)来解决ABA。即乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。

CAS 本身无法直接解决 ABA 问题,但通过一些附加的机制,可以解决这一问题。Java 提供了几种方法来避免 ABA 问题,常用的是使用版本号(`AtomicStampedReference`): ```java import java.util.concurrent.atomic.AtomicStampedReference; public class ABASolutionDemo { public static void main(String[] args) { // 初始值为 100,版本号为 1 AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1); int[] stampHolder = new int[1]; // 获取当前值版本号 Integer currentValue = stampedReference.get(stampHolder); int currentStamp = stampHolder[0]; // 模拟其他线程修改操作 new Thread(() -> { int[] stampHolder1 = new int[1]; Integer value1 = stampedReference.get(stampHolder1); int stamp1 = stampHolder1[0]; // 将值从 100 改为 101 stampedReference.compareAndSet(value1, 101, stamp1, stamp1 + 1); int[] stampHolder2 = new int[1]; Integer value2 = stampedReference.get(stampHolder2); int stamp2 = stampHolder2[0]; // 再将值从 101 改回 100 stampedReference.compareAndSet(value2, 100, stamp2, stamp2 + 1); }).start(); try { // 等待其他线程操作完成 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 尝试更新值 boolean result = stampedReference.compareAndSet(currentValue, 200, currentStamp, currentStamp + 1); if (result) { System.out.println("更新成功,新值为: " + stampedReference.getReference()); } else { System.out.println("更新失败,当前值为: " + stampedReference.getReference()); } } } ``` 在上述代码中,`AtomicStampedReference` 类不仅会比较值,还会比较版本号。每次对值进行修改时,版本号都会增加,这样即使值从 A 变为 B 再变回 A,但版本号已经改变,就可以避免 ABA 问题[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值