CAS操作中的ABA问题以及JAVA中的解决方案

117 阅读2分钟

1. ABA问题的提出

public class ABA {
    static AtomicReference<String> ref = new AtomicReference<>("A");
    public static void main(String[] args) throws InterruptedException {
        String prev = ref.get();
        other();
        Thread.sleep(1 * 1000);
        System.out.println("主线程成功了吗" + ref.compareAndSet("A", "C"));
    }
    private static void other(){
        new Thread(){
            @Override
            public void run() {
                System.out.println("子线程1成功了吗" + ref.compareAndSet("A", "B"));
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                System.out.println("子线程2成功了吗" + ref.compareAndSet("B", "A"));
            }
        }.start();
    }
}

上面这个代码可以描述CAS中的ABA问题,在A修改为C的过程中,这个参数可能已经发生过变化了。在上面的代码可以看出这个ref参数在other的两个线程里面被修改了两次。但是主线程仍然成功把这个A变为了C。这个是存在风险的。

2. ABA问题的解决方案

2.1 版本号机制

package org.example.juclearning.learning02;

import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABA {
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);
    public static void main(String[] args) throws InterruptedException {
        String prev = ref.getReference();
        int stamp = ref.getStamp();
        other();
        Thread.sleep(1 * 1000);
        System.out.println("主线程成功了吗" + ref.compareAndSet(prev, "C",stamp,stamp+1));
    }
    private static void other(){
        new Thread(){
            @Override
            public void run() {
                int stamp = ref.getStamp();
                System.out.println("子线程1成功了吗" + ref.compareAndSet(ref.getReference(), "B",stamp,stamp+1));
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                int stamp = ref.getStamp();
                System.out.println("子线程1成功了吗" + ref.compareAndSet(ref.getReference(), "A",stamp,stamp+1));
            }
        }.start();
    }
}

在上面的代码中采用了AtomicStampedReference类来解决这个问题,在这个类里main加入类版本号机制。例如在第10行、19行、26行获得当前的版本号。在ref进行CAS操作的时候,不光以prev为参照当前变量是否被修改,同时参考当前对象的版本号是否发生变化。

2.2加入是否修改过标记

package org.example.juclearning.learning02;

import java.util.concurrent.atomic.AtomicMarkableReference;

public class T38 {
    public static void main(String[] args) throws InterruptedException {
        GarbageBag bag = new GarbageBag("装满了垃圾");
        AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag,true);
        GarbageBag prev = ref.getReference();
        new Thread(){
            @Override
            public void run() {
                bag.setDesc("变成空垃圾袋");
                ref.compareAndSet(bag,bag,true,false);
            }
        }.start();
        Thread.sleep(1 * 1000);
        boolean result = ref.compareAndSet(prev, new GarbageBag("新的垃圾袋"), true, false);
        System.out.println("换垃圾袋成功了吗" + result);
    }
}

class GarbageBag{
    String desc;

    public GarbageBag(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "GarbageBag{" +
                "desc='" + desc + ''' +
                '}';
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

类似于版本号标记,观察代码的8、11、18行。