CAS导致的ABA问题及解决

栏目: Java · 发布时间: 6年前

内容简介:在并发问题中,最先想到的无疑是互斥同步,但线程阻塞和唤醒带来了很大的性能问题,同步锁的核心无非是防止共享变量并发修改带来的问题,但不是任何时候都有这样的竞争关系。CAS,比较并交换(Compare-and-Swap,CAS),如果期望值和主内存值一样,则交换要更新的值,也称乐观锁。如线程甲从主内存中拷贝了变量A为1,在自己的线程中将副本A改为了10,当线程甲准备把这个变量更新到主内存时,如果主内存A的值不改变(期望值),还是1,那么线程甲成功更新主内存中A的值。但如果主内存A的值已经先被其他线程改掉不为1,

在并发问题中,最先想到的无疑是互斥同步,但线程阻塞和唤醒带来了很大的性能问题,同步锁的核心无非是防止共享变量并发修改带来的问题,但不是任何时候都有这样的竞争关系。

什么是CAS

CAS,比较并交换(Compare-and-Swap,CAS),如果期望值和主内存值一样,则交换要更新的值,也称乐观锁。

如线程甲从主内存中拷贝了变量A为1,在自己的线程中将副本A改为了10,当线程甲准备把这个变量更新到主内存时,如果主内存A的值不改变(期望值),还是1,那么线程甲成功更新主内存中A的值。但如果主内存A的值已经先被其他线程改掉不为1,那么线程甲不断地重试,直到成功为止(自旋)。

CAS来自哪

CAS属于 J.U.C 包,调用的 Unsafe 类中方法,这是一种硬件支持的原子性操作,不能被打断或停止,无需互斥同步。

以AtomicInteger下的getAndAddInt方法为例,U即Unsafe类。

/**
  * @param expectedValue 期望值
  * @param newValue 新值
  * @return 比价更新是否成功.
  */
public final int incrementAndGet() {
    return U.getAndAddInt(this, VALUE, 1) + 1;
}
复制代码

再往下看,通过 getIntVolatile(o, offset) 得到以前的值v,通过调用 weakCompareAndSetInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 v,那么就更新内存地址为 o+offset的变量为 v + delta。getAndAddInt()方法 在一个循环中进行, 发生冲突的做法是不断的进行重试

/**
     * @param o 更新字段/元素的对象/数组
     * @param offset 字段/元素偏移量
     * @param delta 要添加的值,步长
     * @return 以前的值
     * @since 1.8
     */
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}
复制代码

运行代码示例

线程t1,t2,同时修改主内存的一变量值,人为的让B快与A

public class TestCAS {
    // 主内存atomicInteger初始值为1
    public static AtomicInteger atomicInteger = new AtomicInteger(1);

    public static void main(String[] args) {
        // A线程计划将值改为10,先休眠2s,再比较交换
        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("当前线程: "+Thread.currentThread().getName()+"比较交换结果:"
                    +atomicInteger.compareAndSet(1, 10)+" 现在值为:"+atomicInteger.get());
        },"t1").start();

        // B线程计划将值改为10,不休眠
        new Thread(() -> {
            System.out.println("当前线程: "+Thread.currentThread().getName()+"比较交换结果:"
                    +atomicInteger.compareAndSet(1, 20)+" 现在值为:"+atomicInteger.get());
        },"t2").start();
    }
}
复制代码

控制台

当前线程: t2比较交换结果:true 现在值为:20
当前线程: t1比较交换结果:false 现在值为:20
复制代码

这样不用加同步锁,就实现了变量的并发修改带来的问题。

如果你的好朋友向你借走了10块,第二天他又还给你了10块,如果的你的朋友只是为了买包零食,你可能不会在乎,如何他用那10块中了大奖,你可能会有些着急了...

ABA问题引入

上个代码中,存在一个问题。如:t1,t2线程都拷贝到变量atomicInteger=1,如果B线程优先级较高或运气好,第一次,t2先将atomicInteger修改为20并成功写入主内存,接着t2又拷贝到atomicInteger=20,将副本又改为1,并成功写回主内存。第三次,t1拿到主内存atomicInteger的值。可这个值已经被t2修改过两次,会有问题吗?

CAS导致的ABA问题及解决

ABA问题

如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。

ABA解决

  1. 互斥同步锁synchronized

  2. 如果项目只在乎数值是否正确, 那么ABA 问题不会影响程序并发的正确性。

  3. J.U.C 包提供了一个带有时间戳的原子引用类 AtomicStampedReference 来解决该问题,它通过 控制变量的版本 来保证 CAS 的正确性。

AtomicStampedReference代码示例

public class SolveCAS {
    // 主内存共享变量,初始值为1,版本号为1
    private static AtomicStampedReference<Integer> atomicStampedReference = new
            AtomicStampedReference<>(1, 1);


    public static void main(String[] args) {
        // t1,期望将1改为10
        new Thread(() -> {
            // 第一次拿到的时间戳
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+" 第1次时间戳:"+stamp+" 值为:"+atomicStampedReference.getReference());
            // 休眠5s,确保t2执行完ABA操作
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
            // t2将时间戳改为了3,cas失败
            boolean b = atomicStampedReference.compareAndSet(1, 10, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+" CAS是否成功:"+b);
            System.out.println(Thread.currentThread().getName()+" 当前最新时间戳:"+atomicStampedReference.getStamp()+" 最新值为:"+atomicStampedReference.getReference());
        },"t1").start();

        // t2进行ABA操作
        new Thread(() -> {
            // 第一次拿到的时间戳
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+" 第1次时间戳:"+stamp+" 值为:"+atomicStampedReference.getReference());
            // 休眠,修改前确保t1也拿到同样的副本,初始值为1
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            // 将副本改为20,再写入,紧接着又改为1,写入,每次提升一个时间戳,中间t1没介入
            atomicStampedReference.compareAndSet(1, 20, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+" 第2次时间戳:"+atomicStampedReference.getStamp()+" 值为:"+atomicStampedReference.getReference());
            atomicStampedReference.compareAndSet(20, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName()+" 第3次时间戳:"+atomicStampedReference.getStamp()+" 值为:"+atomicStampedReference.getReference());

        },"t2").start();
    }
}
复制代码

控制台

t1 第1次时间戳:1 值为:1
t2 第1次时间戳:1 值为:1
t2 第2次时间戳:2 值为:20
t2 第3次时间戳:3 值为:1
t1 CAS是否成功:false
t1 当前最新时间戳:3 最新值为:1
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

ActionScript 3.0 Cookbook

ActionScript 3.0 Cookbook

Joey Lott、Darron Schall、Keith Peters / Adobe Dev Library / 2006-10-11 / GBP 28.50

Well before Ajax and Microsoft's Windows Presentation Foundation hit the scene, Macromedia offered the first method for building web pages with the responsiveness and functionality of desktop programs......一起来看看 《ActionScript 3.0 Cookbook》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具