高并发面试必问:CAS 引起ABA问题解决方案

Posted 程序员石磊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高并发面试必问:CAS 引起ABA问题解决方案相关的知识,希望对你有一定的参考价值。

该文章来自《java高并发核心编程》,说来惭愧在阅读该书之前只知道用版本号解决,不知道Jdk已经提供了实现,下面来揭开神秘面试吧!

很多乐观锁的实现版本都是使用版本号(Version)方式来解决ABA问题。乐观锁每次在执行数据的修改操作时都会带上一个版本号,版本号和数据的版本号一致就可以执行修改操作并对版本号执行加1操作,否则执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加,不会减少。

使用AtomicStampedReference解决ABA问题

参考乐观锁的版本号,JDK提供了一个AtomicStampedReference类来解决ABA问题。AtomicStampReference在CAS的基础上增加了一个Stamp(印戳或标记),使用这个印戳可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。

AtomicStampReference的compareAndSet()方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,就以原子方式将引用值和印戳(Stamp)标志的值更新为给定的更新值。AtomicStampReference的构造器有两个参数,具体如下:

    //构造器,V表示要引用的原始数据,initialStamp表示最初的版本印戳(版本号)

     AtomicStampedReference(V initialRef, int initialStamp)

AtomicStampReference常用的几个方法如下:

//获取被封装的数据

     public V getRerference();

     //获取被封装的数据的版本印戳

     public int getStamp();

AtomicStampedReference的CAS操作的定义如下:

  public boolean compareAndSet(

                    V  expectedReference,           //预期引用值

                    V   newReference,                       //更新后的引用值

                   int  expectedStamp,              //预期印戳(Stamp)标志值

                   int  newStamp)                           //更新后的印戳(Stamp)标志值

compareAndSet()方法的第一个参数是原来的CAS中的参数,第二个参数是替换后的新参数,第三个参数是原来CAS数据旧的版本号,第四个参数表示替换后的新参数版本号。

进行CAS操作时,若当前引用值等于预期引用值,并且当前印戳值等于预期印戳值,则以原子方式将引用值和印戳值更新为给定的更新值。

下面是一个简单的AtomicStampedReference使用示例,通过两个线程分别带上印戳更新同一个atomicStampedRef实例的值,第一个线程会更新成功,而第二个线程会更新失败,具体代码如下:

  package com.crazymakercircle.cas;

     // 省略import

     public class AtomicTest

     {

         @Test

         public void testAtomicStampedReference() 

         {

             CountDownLatch latch = new CountDownLatch(2);

             AtomicStampedReference<Integer> atomicStampedRef =

                     new AtomicStampedReference<Integer>(1, 0);

             ThreadUtil.getMixedTargetThreadPool().submit(new Runnable()

             {

                 @Override

                 public void run()

                 {

                     boolean success = false;

                     int stamp = atomicStampedRef.getStamp();

                     Print.tco("before sleep 500: value=" 

                                 + atomicStampedRef.getReference()

                                 + " stamp=" + atomicStampedRef.getStamp());

                     //等待500毫秒

                     sleepMilliSeconds(500);

                     success = atomicStampedRef.compareAndSet(1, 10,

                             stamp, stamp + 1);

                     Print.tco("after sleep 500 cas 1: success=" + success

                                 + " value=" + atomicStampedRef.getReference()

                                 + " stamp=" + atomicStampedRef.getStamp());

                     //增加印戳值,然后更新,如果stamp被其他线程改了,就会更新失败

                     stamp++;

                     success = atomicStampedRef.compareAndSet(10, 1,

                             stamp, stamp+1);

                     Print.tco("after  sleep 500 cas 2: success=" + success

                                 + " value=" + atomicStampedRef.getReference()

                                 + " stamp=" + atomicStampedRef.getStamp());

                     latch.countDown();

                 }

             });

             ThreadUtil.getMixedTargetThreadPool().submit(new Runnable()

             {

                 @Override

                 public void run()

                 {

                     boolean success = false;

                     int stamp = atomicStampedRef.getStamp();

                     // stamp = 0

                     Print.tco("before sleep 1000: value=" 

                                  + atomicStampedRef.getReference()

                                  + " stamp=" + atomicStampedRef.getStamp());

                     //等待1000毫秒

                     sleepMilliSeconds(1000);

                     Print.tco("after sleep 1000: stamp = " 

                                 + atomicStampedRef.getStamp());

                     //stamp = 1,这个值实际已经被修改了

                     success = atomicStampedRef.compareAndSet(

                                                     1, 20, stamp, stamp++);

                     Print.tco("after cas 3 1000: success=" + success

                                  + " value=" + atomicStampedRef.getReference()

                                  + " stamp=" + atomicStampedRef.getStamp());

                     latch.countDown();

                 }

             });

             latch.await();

         }

         // 省略其他

     }

运行以上示例,输出结果如下:

			[apppool-1-mixed-2]:before sleep 1000: value=1 stamp=0

     [apppool-1-mixed-1]:before sleep 500: value=1 stamp=0

     [apppool-1-mixed-1]:after sleep 500 cas 1: success=true value=10 stamp=1

     [apppool-1-mixed-1]:after  sleep 500 cas 2: success=true value=1 stamp=2

     [apppool-1-mixed-2]:after sleep 1000: stamp = 2

     [apppool-1-mixed-2]:after cas 3 1000: success=false value=1 stamp=2

使用AtomicMarkableReference解决ABA问题

AtomicMarkableReference是AtomicStampedReference的简化版,不关心修改过几次,只关心是否修改过。

因此,其标记属性mark是boolean类型,而不是数字类型,标记属性mark仅记录值是否修改过。AtomicMarkableReference适用于只要知道对象是否被修改过,而不适用于对象被反复修改的场景。

下面是一个简单的AtomicMarkableReference使用示例,通过两个线程分别更新同一个atomicRef的值,第一个线程会更新成功,而第二个线程会更新失败,具体代码如下:

 package com.crazymakercircle.cas;

     // 省略import

     public class AtomicTest

     {

       @Test

         public void testAtomicMarkableReference() throws InterruptedException

         {

             CountDownLatch latch = new CountDownLatch(2);

             AtomicMarkableReference<Integer> atomicRef =

                     new AtomicMarkableReference<Integer>(1, false);

             ThreadUtil.getMixedTargetThreadPool().submit(new Runnable()

             {

                 @Override

                 public void run()

                 {

                     boolean success = false;

                     int value = atomicRef.getReference();

                     boolean mark = getMark(atomicRef);

                     Print.tco("before sleep 500: value=" + value

                             + " mark=" + mark);

                     //等待500毫秒

                     sleepMilliSeconds(500);

                     success = atomicRef.compareAndSet(1, 10, mark, !mark);

                     Print.tco("after sleep 500 cas 1: success=" + success

                             + " value=" + atomicRef.getReference()

                             + " mark=" + getMark(atomicRef));

                     latch.countDown();

                 }

             });

             ThreadUtil.getMixedTargetThreadPool().submit(new Runnable()

             {

                 @Override

                 public void run()

                 {

                     boolean success = false;

                     int value = atomicRef.getReference();

                     boolean mark = getMark(atomicRef);

                     Print.tco("before sleep 1000: value=" 

                                  + atomicRef.getReference()

                                  + " mark=" + mark);

                     //等待1000毫秒

                     sleepMilliSeconds(1000);

                     Print.tco("after sleep 1000: mark = " + getMark(atomicRef));

                     success = atomicRef.compareAndSet(1, 20, mark,!mark);

                     Print.tco("after cas 3 1000: success=" + success

                                 + " value=" + atomicRef.getReference()

                                 + " mark=" + getMark(atomicRef));

                     latch.countDown();

                 }

             });

             latch.await();

         }

         //取得修改标志值

         private boolean getMark(AtomicMarkableReference<Integer> atomicRef)

         {

             boolean[] markHolder = {false};

             int value = atomicRef.get(markHolder);

             return markHolder[0];

         }

         // 省略其他

     }

运行以上示例,输出结果如下:

 	 [apppool-1-mixed-1]:before sleep 500: value=1 mark=false

     [apppool-1-mixed-2]:before sleep 1000: value=1 mark=false

     [apppool-1-mixed-1]:after sleep 500 cas 1: success=true value=10 mark=true

     [apppool-1-mixed-2]:after sleep 1000: mark = true

     [apppool-1-mixed-2]:after cas 3 1000: success=false value=10 mark=true

以上是关于高并发面试必问:CAS 引起ABA问题解决方案的主要内容,如果未能解决你的问题,请参考以下文章

面试必问的CAS,你懂了吗?

高并发学习笔记—— CAS 和ABA问题

CAS,在硬件层面为并发安全保驾护航

CAS,在硬件层面为并发安全保驾护航

CAS与ABA问题产生和解决

面试必问的CAS