• 并发编程学习笔记(三十一、ABA问题)


    目录:

    • CAS操作步骤
    • CAS极端情况下可能会导致的问题:ABA
    • ABA问题的影响
    • JUC是如何解决ABA问题的

    CAS操作步骤

    CAS:compare and swap,即比较后再交换。它是一种无锁的算法,操作如下:

    • 从内存获取V的值。
    • 拿到旧的预期值A。
    • 拿到要修改的值B。
    • 当且仅当预期值A和内存值V相同的时候,将内存值V修改为B;否则啥都不做。

    CAS极端情况下可能会导致的问题:ABA

    因为CAS并不是原子操作,所以在并发情况下可能会导致如下问题(当然锁和循环CAS的方式实现原子操作):

    • 1、线程1获取到内存值V。
    • 2、线程2获取到内存值V。
    • 3、线程1将内存值V改成了B。
    • 4、线程2还有一些逻辑没做,还未执行到CAS操作(线程B休息了一会)。
    • 5、线程1又将值A写入了V。
    • 6、线程2执行CAS,发现内存值V与预期值A一致,所以执行了CAS操作,CAS成功。

    此时尽管线程2的CAS操作成功了,单着并不代表这个过程是没有问题的,因为对于线程2而言,线程1执行的修改以及丢失了。

    所以ABA问题只是CAS使用过程中的极端情况,某个线程将值先改成B又改成A,这样另一个线程CAS的时候会发现旧的值还是A,导致数据不对,这就是ABA问题。

    ABA问题的影响

    假如我们现在有一个数据结构底层基于栈实现,按照上面那种情况,就会出现如下的问题:

    首先我们有一个栈,里面有A、B两个元素,A为栈顶(图1)。

    执行到第4步,线程2休眠了一会,此时线程2已经知道A.next = B,将要执行CAS将栈顶替换为B。

    执行第5步,线程1将A、B两个元素一次出栈,并又将D、C、A依次入栈,此时对象B处于游离状态,然后栈中元素见图2、图3。

    最后当线程2执行CAS操作后,发现栈顶还是A,所以CAS成功,将栈顶替换为B;但实际上B.next = null,所以此时栈中只有B一个元素了,C和D就平白无故的丢了

    JUC是如何解决ABA问题的

    上节的AtomicInteger曾说道有两个Atomic类可以避免ABA问题:

    • AtomicMarkableReference:内部通过Pari承载引用对象是否被更新过的标记,避免了BAB问题。
    • AtomicStampedeReference:内部通过Pari承载引用对象更新的邮戳,避免了BAB问题。

    AtomicMarkableReference:

    AtomicMarkableReference:既然ABA问题是因为需要执行CAS操作的线程无法知道值有没有更新过,那我干脆就弄一个标识,让这些线程共享这个标识的值不就可以知道原值有没有被更新过了嘛;而AtomicMarkableReference的实现正是这样的。

    首先我们来看下AtomicMarkableReference是如何使用的:

     1 public class AtomicMarkableReferenceDemo {
     2 
     3     /**
     4      * 初始值
     5      */
     6     private static final Integer INIT_NUM = 10;
     7 
     8     /**
     9      * 临时值
    10      */
    11     private static final Integer TEMP_NUM = 20;
    12 
    13     /**
    14      * 更新值
    15      */
    16     private static final Integer UPDATE_NUM = 100;
    17 
    18     /**
    19      * 更新标识
    20      */
    21     private static final Boolean INITIAL_MARK = Boolean.FALSE;
    22 
    23     private static AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(INIT_NUM, INITIAL_MARK);
    24 
    25     public static void main(String[] args) {
    26         // 线程2,执行CAS操作的线程
    27         new Thread(() -> {
    28             System.out.println(Thread.currentThread().getName() + " : 初始值为:" + INIT_NUM + " , 标记为: " + INITIAL_MARK);
    29             boolean mark = atomicMarkableReference.isMarked();
    30             try {
    31                 Thread.sleep(1000);
    32             }
    33             catch (InterruptedException e) {
    34                 e.printStackTrace();
    35             }
    36             // 执行CAS  10 ---> 100
    37             boolean result = atomicMarkableReference.compareAndSet(INIT_NUM, UPDATE_NUM, mark, Boolean.TRUE);
    38             System.out.println("AtomicMarkableReference发生ABA后的执行结果=" + result);
    39         }, "线程A").start();
    40 
    41         // 线程1,修改内存值的线程
    42         new Thread(() -> {
    43             Thread.yield();
    44             // 初始态
    45             System.out.println(Thread.currentThread().getName() + " : 初始值为:" + atomicMarkableReference.getReference() + " , 标记为: " + INITIAL_MARK);
    46             // CAS修改  10 ---> 20
    47             atomicMarkableReference.compareAndSet(atomicMarkableReference.getReference(), TEMP_NUM, atomicMarkableReference.isMarked(), Boolean.TRUE);
    48             System.out.println(Thread.currentThread().getName() + " : 修改后的值为:" + atomicMarkableReference.getReference() + " , 标记为: " + atomicMarkableReference.isMarked());
    49             // CAS修改  20 ---> 10
    50             atomicMarkableReference.compareAndSet(atomicMarkableReference.getReference(), INIT_NUM, atomicMarkableReference.isMarked(), Boolean.TRUE);
    51             System.out.println(Thread.currentThread().getName() + " : 修改后的值为:" + atomicMarkableReference.getReference() + " , 标记为: " + atomicMarkableReference.isMarked());
    52 
    53         }, "线程B").start();
    54     }
    55 
    56 }

    最终会修改失败,结果如下:

    线程A : 初始值为:10 , 标记为: false
    线程B : 初始值为:10 , 标记为: false
    线程B : 修改后的值为:20 , 标记为: true
    线程B : 修改后的值为:10 , 标记为: true
    AtomicMarkableReference发生ABA后的执行结果=false

    原理很简单,你可以自行翻阅:就是通过Unsafe的compareAndSwapObject实现CAS,通过Pair的mark判断有没有改变值。

     1 public boolean compareAndSet(V       expectedReference,
     2                              V       newReference,
     3                              boolean expectedMark,
     4                              boolean newMark) {
     5     Pair<V> current = pair;
     6     return
     7         expectedReference == current.reference &&
     8         expectedMark == current.mark &&
     9         ((newReference == current.reference &&
    10           newMark == current.mark) ||
    11          casPair(current, Pair.of(newReference, newMark)));
    12 }
     1 private static class Pair<T> {
     2     final T reference;
     3     final boolean mark;
     4     private Pair(T reference, boolean mark) {
     5         this.reference = reference;
     6         this.mark = mark;
     7     }
     8     static <T> Pair<T> of(T reference, boolean mark) {
     9         return new Pair<T>(reference, mark);
    10     }
    11 }

    AtomicStampedeReference:

    AtomicStampedeReference的原理和makrable的原理很相似,但标记值就并非为boolean了,而是int,类似于mysql中版本号的概念,判断前后的版本号是否一致,一致则更新,不一致则失败。

     1 public class AtomicStampedReferenceDemo {
     2 
     3     /**
     4      * AtomicInteger计数器
     5      */
     6     private static AtomicInteger atomicCounter = new AtomicInteger(100);
     7 
     8     /**
     9      * AtomicStampedReference计数器
    10      */
    11     private static AtomicStampedReference<Integer> atomicStampedCounter = new AtomicStampedReference<>(100, 0);
    12 
    13     /**
    14      * 测试代码
    15      */
    16     public static void main(String[] args) throws InterruptedException {
    17         // 测试AtomicInteger不会发现ABA问题
    18         Thread thread1 = new Thread(() -> {
    19             // 100变101
    20             atomicCounter.compareAndSet(100, 101);
    21             // 101变100
    22             atomicCounter.compareAndSet(101, 100);
    23         });
    24         // 测试线程2不会发现ABA问题
    25         Thread thread2 = new Thread(() -> {
    26             try {
    27                 TimeUnit.SECONDS.sleep(1);
    28             }
    29             catch (InterruptedException e) {
    30                 e.printStackTrace();
    31             }
    32             boolean atomicResult = atomicCounter.compareAndSet(100, 101);
    33             System.out.println("发生ABA问题时,AtomicInteger执行结果= " + atomicResult);
    34         });
    35 
    36         thread1.start();
    37         thread2.start();
    38         thread1.join();
    39         thread2.join();
    40 
    41         // 测试AtomicStampedReference会发现ABA问题
    42         Thread stamped1 = new Thread(() -> {
    43             try {
    44                 TimeUnit.SECONDS.sleep(1);
    45             }
    46             catch (InterruptedException e) {
    47                 e.printStackTrace();
    48             }
    49             atomicStampedCounter.compareAndSet(100, 101, atomicStampedCounter.getStamp(), atomicStampedCounter.getStamp() + 1);
    50             atomicStampedCounter.compareAndSet(101, 100, atomicStampedCounter.getStamp(), atomicStampedCounter.getStamp() + 1);
    51             System.out.println("线程stamped1获取的版本号 = " + atomicStampedCounter.getStamp());
    52         });
    53         Thread stamped2 = new Thread(() -> {
    54             // stamp发生变化
    55             int stamp = atomicStampedCounter.getStamp();
    56             System.out.println("线程stamped2在ABA发生前获取的版本号 = " + stamp);
    57             try {
    58                 TimeUnit.SECONDS.sleep(2);
    59             }
    60             catch (InterruptedException e) {
    61                 e.printStackTrace();
    62             }
    63             boolean atomicStampedResult = atomicStampedCounter.compareAndSet(100, 101, stamp, stamp + 1);
    64             System.out.println("发生ABA问题时,AtomicStampedReference执行结果= " + atomicStampedResult);
    65         });
    66 
    67         stamped1.start();
    68         stamped2.start();
    69     }
    70     
    71 }

    原理类似:

     1 public boolean compareAndSet(V   expectedReference,
     2                              V   newReference,
     3                              int expectedStamp,
     4                              int newStamp) {
     5     Pair<V> current = pair;
     6     return
     7         expectedReference == current.reference &&
     8         expectedStamp == current.stamp &&
     9         ((newReference == current.reference &&
    10           newStamp == current.stamp) ||
    11          casPair(current, Pair.of(newReference, newStamp)));
    12 }
     1 private static class Pair<T> {
     2     final T reference;
     3     final int stamp;
     4     private Pair(T reference, int stamp) {
     5         this.reference = reference;
     6         this.stamp = stamp;
     7     }
     8     static <T> Pair<T> of(T reference, int stamp) {
     9         return new Pair<T>(reference, stamp);
    10     }
    11 }

    ——————————————————————————————————————————————————————————————————————

  • 相关阅读:
    移动Web开发调研
    如何将一个Excel文件中的sheet移动到另外一个Excel?
    Web自动化测试工具调研
    DOM中文本节点索引方法
    词法、语法与语义相关知识
    http 登录Digest认证相关知识
    javascript sandbox
    MVVM与Backbone demo
    Sass与Web组件化相关的功能
    Lua参数绑定函数实现方法
  • 原文地址:https://www.cnblogs.com/bzfsdr/p/13324718.html
Copyright © 2020-2023  润新知