AtomicStampedReference解决ABA问题

AtomicStampedReference解决ABA问题
本文探讨了在使用CAS进行无锁操作时遇到的ABA问题,并介绍了AtomicStampedReference如何通过版本戳来解决这一问题,避免了并发操作带来的隐患。

AtomicStampedReference解决ABA问题

在运用CAS做Lock-Free操作中有一个经典的ABA问题:

线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题,例如下面的例子:

AtomicStampedReference解决ABA问题 - 木瓜仙人 - 木瓜仙人

现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

head.compareAndSet(A,B);

在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:

AtomicStampedReference解决ABA问题 - 木瓜仙人 - 木瓜仙人

此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:

AtomicStampedReference解决ABA问题 - 木瓜仙人 - 木瓜仙人

其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败:

public class Test {

    private static AtomicInteger atomicInt = new AtomicInteger(100);

    private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0);

 

    public static void main(String[] args) throws InterruptedException {

       Thread intT1 = new Thread(new Runnable() {

           @Override

           public void run() {

              atomicInt.compareAndSet(100, 101);

              atomicInt.compareAndSet(101, 100);

           }

       });

 

       Thread intT2 = new Thread(new Runnable() {

           @Override

           public void run() {

              try {

                  TimeUnit.SECONDS.sleep(1);

              } catch (InterruptedException e) {

              }

              boolean c3 = atomicInt.compareAndSet(100, 101);

              System.out.println(c3); // true

           }

       });

 

       intT1.start();

       intT2.start();

       intT1.join();

       intT2.join();

 

       Thread refT1 = new Thread(new Runnable() {

           @Override

           public void run()

              try {

                  TimeUnit.SECONDS.sleep(1);

              } catch (InterruptedException e) {

              }

              atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);

              atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);

           }

       });

 

       Thread refT2 = new Thread(new Runnable() {

           @Override

           public void run() {

              int stamp = atomicStampedRef.getStamp();

              try {

                  TimeUnit.SECONDS.sleep(2);

              } catch (InterruptedException e) {

              }

              boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);

              System.out.println(c3); // false

           }

       });

 

       refT1.start();

       refT2.start();

    }

}


### 解决ABA问题的方案 在并发编程中,ABA问题是CAS(Compare-And-Swap)算法的一个潜在缺陷。当一个线程两次读取某个内存地址时,如果读到的值相同,则CAS会认为该值在这段时间内未被修改。然而,实际上可能另一个线程在此期间对该值进行了修改,例如从A改为B,再改回A[^4]。 为了解决ABA问题,通常采用以下几种方法: #### 1. 增加版本号机制 通过引入版本号或时间戳来标记变量的每次修改。每次修改变量时,不仅更新其值,还同时增加版本号。这样,在进行CAS操作时,不仅要比较变量的值,还要比较版本号。如果版本号不同,则说明变量在两次读取之间发生了变化,即使最终值恢复为原来的值[^2]。 ```java // 示例代码:使用版本号解决ABA问题 public class AtomicStampedReference<V> { private volatile int stamp; private volatile V reference; public AtomicStampedReference(V initialRef, int initialStamp) { this.reference = initialRef; this.stamp = initialStamp; } public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { if (reference == expectedReference && stamp == expectedStamp) { reference = newReference; stamp = newStamp; return true; } return false; } } ``` #### 2. 使用`AtomicStampedReference` Java提供了`AtomicStampedReference`类来解决ABA问题。该类通过引入一个额外的整数字段(称为“标记”或“版本号”)来记录变量的变化次数。每次修改变量时,标记也会随之递增。这样可以确保即使变量的值恢复到原始值,标记仍然不同,从而避免ABA问题[^3]。 ```java // 示例代码:使用AtomicStampedReference解决ABA问题 import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicStampedReference; public class ABAExample { public static void main(String[] args) throws InterruptedException { AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 0); Thread t1 = new Thread(() -> { int stamp = atomicStampedRef.getStamp(); atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1); atomicStampedRef.compareAndSet(101, 100, stamp + 1, stamp + 2); }); Thread t2 = new Thread(() -> { try { Thread.sleep(1000); // 等待t1完成一次修改 } catch (InterruptedException e) { e.printStackTrace(); } int[] stampHolder = new int[1]; stampHolder[0] = atomicStampedRef.getStamp(); atomicStampedRef.compareAndSet(100, 102, stampHolder[0], stampHolder[0] + 1); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Final value: " + atomicStampedRef.getReference()); System.out.println("Final stamp: " + atomicStampedRef.getStamp()); } } ``` #### 3. 使用锁机制 虽然锁机制可能会降低程序的性能,但它可以有效避免ABA问题。通过使用互斥锁,确保在同一时间只有一个线程能够访问和修改共享变量,从而避免其他线程对变量进行修改[^5]。 --- ### 总结 解决ABA问题的核心思想是引入额外的信息来区分变量的变化过程。无论是通过版本号机制、`AtomicStampedReference`类,还是锁机制,都可以有效地避免ABA问题带来的潜在风险。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值