cas

作者: 布莱安托 | 来源:发表于2020-09-12 18:17 被阅读0次

    CAS,即Compare-And-Swap比较并交换,它是一条CPU并发原语

    它的功能是判断内存某个位置的值是否为预期值,如果是则改为新的值,这个过程是原子的

    通过AtomInteger举例:

    public class CasDemo {
        
        public static void main(String[] args) {
    
            compareAndSet();
    
        }
    
        private static void compareAndSet() {
    
            AtomicInteger integer = new AtomicInteger(4);
    
            for (int i = 0; i < 5; i++) {
                final int n = i;
                new Thread(() -> {
                    // 比较并设置,如果主内存中的值与期望值相同,则设置新值并返回true,否则返回false
                    boolean result = integer.compareAndSet(4, n);
                    System.out.println(
                            Thread.currentThread().getName() + " result: " + result + ", current value: " + integer.get());
                }, "thread-" + i).start();
            }
    
        }
    
    }
    

    结果:

    thread-0 result: true, current value: 0
    thread-3 result: false, current value: 0
    thread-4 result: false, current value: 0
    thread-2 result: false, current value: 0
    thread-1 result: false, current value: 0

    可见只有一个线程设置新值成功,多个线程对同一变量的操作具有原子性

    compareAndSet方法是如何做到在多线程环境下,安全的对变量比较并设置值的呢?

    通过查看源码我们得知

    /**
    * Atomically sets the value to the given updated value
    * if the current value {@code ==} the expected value.
    *
    * @param expect the expected value
    * @param update the new value
    * @return {@code true} if successful. False return indicates that
    * the actual value was not equal to the expected value.
    */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    

    方法调用的是unsafe.compareAndSwapInt方法,而unsafe这个对象又是什么呢?

    public class AtomicInteger extends Number implements java.io.Serializable {
        private static final long serialVersionUID = 6214790243416807050L;
    
        // setup to use Unsafe.compareAndSwapInt for updates
        private static final Unsafe unsafe = Unsafe.getUnsafe();
        private static final long valueOffset;
    
        static {
            try {
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
    
        private volatile int value;
    

    看到AtomInteger类有这么几个变量:

    1. Unsafe unsafe
    2. long valueOffset
    3. volatile int value

    Unsafe类存在于rt.jar中的sun.misc包中,是CAS的核心类,由于Java无法直接访问操作系统底层,需要通过本地方法(native)来访问,Unsafe类就是Java访问系统底层的一种方式,可以直接操作特定内存中的数据

    valueOffset表示该变量在内存中的内存偏移地址Unsafe类就是通过内存偏移地址获取数据

    value是变量的值,利用volatile修饰后保证了多线程间内存的可见性

    CAS并发原语体现在Java语言中就是sum.misc.Unsafe类中的各个方法,调用Unsafe类中的方法时,JVM会实现CAS汇编指令,调用Unsafe类的方法实际就是执行一套CPU原子指令,是不允许被终端的,不会造成数据不一致的问题

    再来看AtomIntegercompareAndSet方法

    // AtomInteger
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    

    可以看到AtomIntegercompareAndSet方法调用的是UnsafecompareAndSwapInt方法

    // Unsafe
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    

    UnsafecompareAndSwapInt方法是一个本地方法,大致就是对比var1对象的var2地址的数据和期望值var4,如果相等就修改为var5

    CAS的缺点

    1. 长时间循环操作的开销大

      看一下Unsafe类中的getAndAddInt方法

      public final int getAndAddInt(Object var1, long var2, int var4) {
          int var5;
          do {
              var5 = this.getIntVolatile(var1, var2);
          } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
      
          return var5;
      }
      

      当CAS失败时,会循环尝试,如果CAS长时间不成功,可能会给CPU带来性能开销

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

      当对一个共享变量进行操作时,我么你可以使用循环CAS的方式保证原子操作,但当对多个共享变量操作时,循环CAS无法保证操作的原子性,只能用锁来保证

    3. 会引出ABA问题

    ABA问题

    当T1、T2两个线程同时访问一个变量时,两线程同时取出变量N为A,假设T2执行比T1快,T2修改值为B,然后再将其修改为A,此时T1线程进行CAS发现内存中值仍然是A,然后更新至成功

    尽管T1线程操作执行成功,但是不代表过程没有问题

    先来看一下原子引用(AtomicReference)

    public class AtomicReferenceDemo {
    
        public static void main(String[] args) {
    
            Person p1 = new Person("p1", 10);
            Person p2 = new Person("p2", 20);
    
            AtomicReference<Person> atomicReference = new AtomicReference<>();
            atomicReference.set(p1);
    
            System.out.println(atomicReference.compareAndSet(p1, p2) + " ref: " + atomicReference.get());
            System.out.println(atomicReference.compareAndSet(p1, p2) + " ref: " + atomicReference.get());
    
        }
    
    }
    
    class Person {
    
        public String name;
        public int age;
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    

    结果:

    true ref: Person{name='p2', age=20}
    false ref: Person{name='p2', age=20}

    原子引用就是将自定义的引用类型变为原子的引用类型,实现对引用类型数据的原子操作

    与其他类一样,原子引用也会带来ABA问题

    public class AbaDemo {
    
        private static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    
        public static void main(String[] args) {
    
            atomicRef();
    
        }
    
        private static void atomicRef() {
    
            new Thread(() -> {
                atomicReference.compareAndSet(100, 101);
                atomicReference.compareAndSet(101, 100);
            }, "thread-1").start();
    
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(atomicReference.compareAndSet(100, 200) + " ref: " + atomicReference.get());
            }, "thread-2").start();
    
        }
    
    }
    

    结果:

    true ref: 200

    我们可以通过对原子引用变量添加一个版本号,通过比较版本号是否相同来判断数据是否被修改过,这就是AtomicStampedReference

    public class AbaDemo {
    
        private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 0);
    
        public static void main(String[] args) {
    
            atomicStampedRef();
    
        }
    
        private static void atomicRef() {
    
            new Thread(() -> {
                atomicReference.compareAndSet(100, 101);
                atomicReference.compareAndSet(101, 100);
            }, "thread-1").start();
    
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(atomicReference.compareAndSet(100, 200) + " ref: " + atomicReference.get());
            }, "thread-2").start();
    
        }
    
        private static void atomicStampedRef() {
    
            new Thread(() -> {
    
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + " stamp-1: " + stamp);
    
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
                stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + " stamp-2: " + stamp);
    
                atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
                stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + " stamp-3: " + stamp);
    
            }, "thread-1").start();
    
            new Thread(() -> {
    
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + " stamp: " + stamp);
    
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                boolean res = atomicStampedReference.compareAndSet(100, 200, stamp, stamp + 1);
                stamp = atomicStampedReference.getStamp();
    
                System.out.println(Thread.currentThread().getName() + " res:" + res + " stamp: " + stamp);
                System.out.println(Thread.currentThread().getName() + " ref:" + atomicStampedReference.getReference());
    
            }, "thread-2").start();
    
        }
    
    }
    

    结果:

    thread-1 stamp-1: 0
    thread-2 stamp: 0
    thread-1 stamp-2: 1
    thread-1 stamp-3: 2
    thread-2 res:false stamp: 2
    thread-2 ref:100

    可以看到两个线程第一次获取stamp均为0,然后thread-1进行了一次ABA操作,之后stamp(也就是stamp-3)变为了3,在当thread-2按照之前获取的版本号去更新数据时,虽然数据值仍然为100但是版本号已经变更,所以更新失败

    相关文章

      网友评论

          本文标题:cas

          本文链接:https://www.haomeiwen.com/subject/cbtgsktx.html