JUC——CAS

本文详细介绍了CAS(Compare and Swap)操作在多线程编程中的应用及其存在的ABA问题。通过示例代码展示了ABA问题的产生过程,并提出了AtomicStampedReference作为解决方案。AtomicStampedReference通过版本号来避免ABA问题,确保了在并发环境下的正确性。此外,还讨论了CAS仅能保证单个变量原子操作的限制以及循环带来的开销问题。
摘要由CSDN通过智能技术生成

1. CAS

在多线程编程时,如果想保证一段代码具有原子性,通过会使用锁来解决,而CAS是通过硬件指令来达到比较并交换的过程;

CAS原理

CAS包括三个值:
 V:内存地址;
 A:期望值;
 B:新值;
 如果这个内存地址V的值和期望值A相等,则将其赋值为B;

2 CAS存在的问题

2.1 ABA问题

在多线程并发场景下:线程A、B、C 同时对资源 R=1进行修改,线程A期望将R修改为2,在修改之前,线程B拿到CPU资源将R修改为3,线程B结束后,线程C拿到CPU资源将R修改为1,此时线程A接着去做修改,成功了,在线程A看来,R=1一直没有被修改,所以造成ABA问题;

staticFieldOffset 方法用于获取静态属性 Field 在 Class 对象中的偏移量,在 CAS 操作静态属性时,会用到这个偏移量。
objectFieldOffset 方法用于获取非静态 Field (非静态属性)在 Object 实例中的偏移量,在 CAS 操作对象的非静态属性时,会用到这个偏移量。问题复现:*

public class CASTest {
    private static Unsafe unsafe;
    private static long valueOffset;
    static {
        Field theUnsafe = null;
        try {
            theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);

            valueOffset = unsafe.objectFieldOffset(CASTest.class.getDeclaredField("value"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private volatile int value=1;

    public static void main(String[] args) throws Exception{
        final CASTest casTest = new CASTest();
        casTest.testABA(casTest);
    }
    private void testABA(final CASTest casTest){
        final Thread threadB = new Thread(new Runnable() {
            public void run() {
                System.out.println("开始前线程B:"+casTest.value);
                int expect = 1;
                int update = 3;
                System.out.println("准备更新时线程B:"+casTest.value);
                System.out.println("线程B:"+unsafe.compareAndSwapInt(casTest, valueOffset, expect, update));
            }
        });
        final Thread threadC = new Thread(new Runnable() {
            public void run() {
                System.out.println("开始前线程C:"+casTest.value);
                try {
                    threadB.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int expect = 3;
                int update = 1;
                System.out.println("准备更新时线程C:"+casTest.value);
                System.out.println("线程C:"+unsafe.compareAndSwapInt(casTest, valueOffset, expect, update));
            }
        });
        final Thread threadA = new Thread(new Runnable() {
            public void run() {
                System.out.println("开始前线程A:"+casTest.value);
                try {
                    threadC.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int expect = 1;
                int update = 2;
                System.out.println("准备更新时线程A:"+casTest.value);
                System.out.println("线程A:"+unsafe.compareAndSwapInt(casTest, valueOffset, expect, update));
            }
        });
        threadA.start();
        threadC.start();
        threadB.start();
    }
}
输出结果:
开始前线程A1
开始前线程C1
开始前线程B1
准备更新时线程B1
线程B:true
准备更新时线程C3
线程C:true
准备更新时线程A1
线程A:true

通过控制线程执行顺序,复现此问题,但是我们发现如果是对于数值类型的ABA问题不是问题;

来试一下compareAndSwapObject,同样让它产生ABA问题,看是否有影响:

public class CASTest {
    private static Unsafe unsafe;
    private static long valueOffset;
    static {
        Field theUnsafe = null;
        try {
            theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);

            valueOffset = unsafe.objectFieldOffset(CASTest.class.getDeclaredField("value"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private volatile Student value= new Student("");

    public static void main(String[] args) throws Exception{
        final CASTest casTest = new CASTest();
        casTest.testABA(casTest);
    }
    private void testABA(final CASTest casTest){
        final Thread threadB = new Thread(new Runnable() {
            public void run() {
                System.out.println("开始前线程B:"+casTest.value);
                System.out.println("准备更新时线程B:"+casTest.value);
                System.out.println("线程B:"+
                        unsafe.compareAndSwapObject(casTest,
                                valueOffset,
                                casTest.value,
                                new Student("tom")));
            }
        });
        final Thread threadC = new Thread(new Runnable() {
            public void run() {
                System.out.println("开始前线程C:"+casTest.value);
                Student expect = casTest.value;
                try {
                    threadB.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("准备更新时线程C:"+casTest.value);
                System.out.println("线程C:"+unsafe.compareAndSwapObject(
                        casTest,
                        valueOffset,
                        casTest.value,
                        new Student("")));
            }
        });
        final Thread threadA = new Thread(new Runnable() {
            public void run() {
                System.out.println("开始前线程A:"+casTest.value);
                try {
                    threadC.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("准备更新时线程A:"+casTest.value);
                System.out.println("线程A:"+unsafe.compareAndSwapObject(
                        casTest,
                        valueOffset,
                        casTest.value,
                        new Student("ThreadA")));
            }
        });
        threadA.start();
        threadC.start();
        threadB.start();
    }
}

class Student{
    private String name;
    public Student(String name){
        this.name=name;
    }
}
开始前线程Acom.didichuxing.erp.srmsync.juc.Student@6fe706ec
开始前线程Ccom.didichuxing.erp.srmsync.juc.Student@6fe706ec
开始前线程Bcom.didichuxing.erp.srmsync.juc.Student@6fe706ec
准备更新时线程Bcom.didichuxing.erp.srmsync.juc.Student@6fe706ec
线程B:true
准备更新时线程Ccom.didichuxing.erp.srmsync.juc.Student@40d974e1
线程C:true
准备更新时线程Acom.didichuxing.erp.srmsync.juc.Student@6120f560
线程A:true

可以看到Student实例已经发生了多次变化,volatile Student value属于多线程内存可见,当其他线程对value做了修改,另外的线程是能获取到最新值的,去做compareAndSwapObject是成功的,但如果这样:;

final Thread threadA = new Thread(new Runnable() {
    public void run() {
        System.out.println("开始前线程A:"+casTest.value);
        Student student = casTest.value;
        try {
            threadC.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("准备更新时线程A:"+casTest.value);
        System.out.println("线程A:"+unsafe.compareAndSwapObject(
                casTest,
                valueOffset,
                student,
                new Student("ThreadA")));
    }
});

结果肯定是false,那既然是false,引用类型会产生ABA问题吗?个人理解,我觉得引用类型不会产生ABA问题,但如果另外一个线程对value的age字段做了修改,会不会产生问题,我们试一下:

final Thread threadC = new Thread(new Runnable() {
    public void run() {
        casTest.value.age=1;

    }
});
final Thread threadA = new Thread(new Runnable() {
    public void run() {
        System.out.println("开始前线程A:"+casTest.value);
        Student student = casTest.value;
        try {
            threadC.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("准备更新时线程A:"+casTest.value);
        System.out.println("线程A:"+unsafe.compareAndSwapObject(
                casTest,
                valueOffset,
                student,
                new Student("ThreadA")));
    }
});
开始前线程A{name:,age:0}
准备更新时线程A{name:,age:1}
线程A:true

这倒是产生了另外一个问题,如果预期和内存中的student是同一个地址,但是成员变量已经产生了变化,同样更新成功了;

2.2 ABA问题解决办法

总结一下上面提到的ABA问题:

  1. 数值类型ABA问题;
  2. 引用类型对象成员变量被改变时,与内存中的不一致,任然能够修改成功;

ABA问题解决:

Integer initValue = 100;
Integer initVersion = 1;
final AtomicStampedReference<Integer> reference = new AtomicStampedReference<Integer>(initValue,initVersion);

final Thread threadA = new Thread(new Runnable() {
    public void run() {
        int stamp = reference.getStamp();
        System.out.println("线程A:" +
                reference.compareAndSet(100, 200, stamp, stamp + 1));
    }
});

final Thread threadB = new Thread(new Runnable() {
    public void run() {
        int stamp = reference.getStamp();
        try {
            threadA.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程B:" +
                reference.compareAndSet(200, 300, stamp, stamp + 1));
    }
});
threadB.start();
threadA.start();

AtomicStampedReference 原理:

public class AtomicStampedReference<V> {

    // 定义引用类型,包装值和版本号;
    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;

    // 比较并交换
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            // 先做一次校验,如果在这里都已经不一致,则直接返回false,这里没有加锁,那么它可能会存在并发;
            // 可能会有两个线程同时进来,判断并且都成立,则两个线程都会进入到:casPair方法;
            // Pair<V> current = pair; 多个线程进入到compareAndSet方法时,都已经保留了当前的pair值,那如果pair被其他线程修改,则另外一个线程去做cas的时候一定会返回false,所以这块是通过这种方式来防止并发的;
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

    private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
    private static final long pairOffset =
        objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);

    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }

    static long objectFieldOffset(sun.misc.Unsafe UNSAFE,
                                  String field, Class<?> klazz) {
        try {
            return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));
        } catch (NoSuchFieldException e) {
            // Convert Exception to corresponding Error
            NoSuchFieldError error = new NoSuchFieldError(field);
            error.initCause(e);
            throw error;
        }
    }
}

2.3 只能保证一个共享变量的原子操作

至于这个问题,可以参考:AtomicStampedReference 将多个变量封装成对象,再对对象做CAS;

2.4 循环时间长开销大

关于这个问题,我觉得应该还好吧,可以根据服务器配置查询MIPS指标,CPU每秒执行指令数是远远超过CAS的并发;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值