美文网首页
使用AtomicStampedReference解决ABA问题时

使用AtomicStampedReference解决ABA问题时

作者: eliteTyc | 来源:发表于2020-06-11 11:12 被阅读0次

多线程模型:

image.png
每个线程都有自己的独立内存空间,当线程需要操作主内存中的数据,需要先拷贝一份数据到自己的内存空间,然后进行修改,再刷回主内存
CAS:
CompareAndSwap,比较然后替换,多线程解决数据错乱的方案,如上图,试想,i=10表示10本书,t1,t2线程同时进行销售书,如果某一时间,t1和t2线程同时将i=10拷贝到自己的数据空间,t1将书减1,i=9再刷回主内存,即现在书的数目已经为9了,然后t2线程将的工作内存还为10它也进行减1然后刷回主内存,这样主内存的i值还是为9,这样就导致了已经卖了两本书,但是书的数目却只减少了1,CAS便是在t2线程进行减了操作后,需要刷回主内存的时候,将取到的值与主内存的值作比较,相同再进行设置,不同则设置失败。
ABA:
有了CAS可以保证操作数目一致,但是也出现一个问题,例如,t1线程先将书的数目减了1然后别人退还一本书,书的数目又加了1,也就是数的数目经历了10->9->10,然后t2线程操作时,发现主内存还是为10,所以觉得没问题就进行操作了,可能这个例子不能很好的描述问题,但是在某些案例中,会给人一种狸猫换太子的感觉,贴一个图
image.png
图片来自:https://baijiahao.baidu.com/s?id=1648077822185803003&wfr=spider&for=pc

一个小偷,把别人家的钱偷了之后又还了回来,还是原来的钱吗,你老婆出轨之后又回来,还是原来的老婆吗?ABA问题也一样,如果不好好解决就会带来大量的问题。最常见的就是资金问题,也就是别人如果挪用了你的钱,在你发现之前又还了回来。但是别人却已经触犯了法律。
解决方案:

AtomicStampedReference

主要思想:通过加版本控制来进行修改,代码如下:

public class AtomicStampedReferenceTest {

// 初始化,值为100,版本为1
    static AtomicStampedReference<Integer> integerReference =
            new AtomicStampedReference<>(100,1);

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            int stamp =  integerReference.getStamp();
            System.out.println("t1第一次版本:"+stamp);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(100, 101,
                    stamp, stamp + 1));
            stamp = integerReference.getStamp();
            System.out.println("t1第二次版本:"+stamp);
            System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(101,100,
                    stamp,stamp+1));

        },"t1");
        Thread t2 = new Thread(() -> {
            int stamp = integerReference.getStamp();
            System.out.println("t2第一次版本:"+stamp);
//            睡眠3秒保证t1完成一次ABA操作
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2修改成功?:"+integerReference.compareAndSet(100,500,
                    stamp,stamp+1));
            System.out.println("t2:期望版本是:"+stamp+"--当前版本是:"+integerReference.getStamp());
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}
  • 代码解析:
    1. 首先t1,t2线程同时拿到对象的最初版本1,值为100
      2.t2 线程等待3秒,保证t1线程将值修改为101,然后再将值修改为100,但是版本已经从,1->2->3
      3.t2线程根据3秒前拿到的版本进行修改,即使数据值都为100,但是3秒前的版本是1,现在的版本为3,所以修改失败
      打印输出:

t1第一次版本:1
t2第一次版本:1
t1第一次修改成功?:true
t1第二次版本:2
t1第一次修改成功?:true
t2修改成功?:false
期望版本是:1--当前版本是:3

问题:

一切看上去好像都没什么问题,但是当我们把初始值改成大于127的数值,代码如下,修改的地方已经标注,其实只是将里面的100全部替换为400

public class AtomicStampedReferenceTest {

    static AtomicStampedReference<Integer> integerReference =
            // 将100改成400
            new AtomicStampedReference<>(400,1);

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            int stamp =  integerReference.getStamp();
            System.out.println("t1第一次版本:"+stamp);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 将100改成400
            System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(400, 101,
                    stamp, stamp + 1));
            stamp = integerReference.getStamp();
            System.out.println("t1第二次版本:"+stamp);
            System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(101,400,
                    stamp,stamp+1));

        },"t1");
        Thread t2 = new Thread(() -> {
            int stamp = integerReference.getStamp();
            System.out.println("t2第一次版本:"+stamp);
//            睡眠3秒保证t1完成一次ABA操作
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //将100改成400
            System.out.println("t2修改成功?:"+integerReference.compareAndSet(400,500,
                    stamp,stamp+1));
            System.out.println("t2:期望版本是:"+stamp+"--当前版本是:"+integerReference.getStamp());
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

打印输出

t1第一次版本:1
t2第一次版本:1
t1第一次修改成功?:false
t1第二次版本:1
t1第一次修改成功?:false
t2修改成功?:false
t2:期望版本是:1--当前版本是:1

???怎么会呢?我卖100本书让我卖,我卖400本书就不让我卖了?
看看方法

public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            // 这一行重点,期望值当前值对比,也就是我们传的100和400
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
  • 上面标注的一行为原因所在,这里需要将两个Integer对象进行对比,相等则true否则false,那为啥我们传100就可以400就不行,原因就是Integer的缓存,先看下面的代码与打印输出
        Integer a =127;
        Integer b =127;
        System.out.println(a==b); // 输出true
        a=128;
        b=128;
        System.out.println(a==b);// 输出false
        a=-127;
        b=-127;
        System.out.println(a==b);// 输出true
        a=-128;
        b=-128;
        System.out.println(a==b);// 输出true
        a=-129;
        b=-129;
        System.out.println(a==b);// 输出false

主要原因就是Integer的缓存范围为:[-128,127],Integer用==进行值比较,什么时候相等,什么时候不等?
,即只要你的数值在这个范围内使用上面的方法都没错,但是超过了,则每次修改就会失败

解决方案:使用integer来接收当前的对象,在当前对象的基础上进行修改,即使用100与400的地方都改成通过integerReference.getReference()直接获取当前对象,如果直接传400就会导致Integer装包时创建新的对象,导致数值虽然相等,但是对象不相等。

public class AtomicStampedReferenceTest {

    static AtomicStampedReference<Integer> integerReference =
            new AtomicStampedReference<>(400,1);

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            int stamp =  integerReference.getStamp();
            System.out.println("t1第一次版本:"+stamp);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(integerReference.getReference(), 101,
                    stamp, stamp + 1));
            stamp = integerReference.getStamp();
            System.out.println("t1第二次版本:"+stamp);
            System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(integerReference.getReference(),400,
                    stamp,stamp+1));

        },"t1");
        Thread t2 = new Thread(() -> {
            int stamp = integerReference.getStamp();
            System.out.println("t2第一次版本:"+stamp);
//            睡眠3秒保证t1完成一次ABA操作
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2修改成功?:"+integerReference.compareAndSet(integerReference.getReference(),500,
                    stamp,stamp+1));
            System.out.println("t2:期望版本是:"+stamp+"--当前版本是:"+integerReference.getStamp());
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

相关文章

网友评论

      本文标题:使用AtomicStampedReference解决ABA问题时

      本文链接:https://www.haomeiwen.com/subject/rzivtktx.html