美文网首页
关于Java中CompareAndSwap(CAS)的一些理解

关于Java中CompareAndSwap(CAS)的一些理解

作者: leilifengxingmw | 来源:发表于2019-03-26 23:19 被阅读0次

    以AtomicInteger为例,jdk版本1.8

    先举个例子

    public class AtomicIntegerTest {
    
        private static int threadCount = 10;
        private static CountDownLatch countDown = new CountDownLatch(threadCount);
        private static int count = 0;
    
        public static void main(String[] args) {
            Thread[] threads = new Thread[threadCount];
            for (int i = 0; i < threadCount; i++) {
                threads[i] = new Thread(new Counter());
            }
            for (int i = 0; i < threadCount; i++) {
                threads[i].start();
            }
            try {
                countDown.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("count=" + count);
        }
    
        private static class Counter implements Runnable {
    
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    count++;
                }
                countDown.countDown();
            }
        }
    }
    

    在这个例子中,我们开启了10个线程,来增加count的值,期待最后输出的结果是10000。显然,并不是每次运行的结果都是10000。因为多个线程对count的修改操作并不是原子操作。

    某次运行的输出结果

    count=9262
    

    我们可以使用AtomicInteger来解决这个问题。

    public class AtomicIntegerTest {
    
        private static int threadCount = 10;
        private static CountDownLatch countDown = new CountDownLatch(threadCount);
        private static AtomicInteger count = new AtomicInteger(0);//原子操作类
    
        public static void main(String[] args) {
            Thread[] threads = new Thread[threadCount];
            for (int i = 0; i < threadCount; i++) {
                threads[i] = new Thread(new Counter());
            }
            for (int i = 0; i < threadCount; i++) {
                threads[i].start();
            }
            try {
                countDown.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("count=" + count);
        }
    
        private static class Counter implements Runnable {
    
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    //原子操作
                    count.getAndIncrement();
                }
                countDown.countDown();
            }
        }
    }
    

    现在每次打印的结果都是10000。因为AtomicInteger类的getAndIncrement方法是原子操作。

    运行的输出结果

    count=10000
    

    AtomicInteger类中的一些变量

        //unsafe实例,用来获取并操作内存的数据。
        private static final Unsafe unsafe = Unsafe.getUnsafe();
    
        //用来记录偏移量,这是一个final变量
        private static final long valueOffset;
    
        static {
            try {
                //valueOffset默认值是0
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
    
        //value,存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。
        private volatile int value;
    
    

    构造函数

    /**
     * 使用0作为初始值来构建AtomicInteger实例
     */
    public AtomicInteger() {
    }
    
    /**
     * 使用指定的初始值构建AtomicInteger实例
     * 
     * @param initialValue 初始值
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    

    AtomicInteger的getAndIncrement方法

    /**
      * 原子性的把当前值加1。
      *
      * @return 返回以前的值
      */
    public final int getAndIncrement() {
       return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    

    Unsafe的getAndAddInt方法

    /**
     * 原子性的更新一个对象在偏移量为offset处的成员变量的值,或者原子性的更新一个数组在偏移量为offset处的元素的值。
     *
     * @param o 更新成员变量的对象,或者更新元素的数组
     * @param offset 成员变量或者数组元素的偏移
     * @param delta 要增加到的量
     * @return 先前的值
     * @since 1.8
     */
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        //do while 自旋操作
        do {
            //获取AtomicInteger对象在内存中偏移量为offset处的值
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }
    
    

    Unsafe的getIntVolatile方法

    /** Volatile version of {@link #getInt(Object, long)}  */
    public native int getIntVolatile(Object o, long offset);
    

    Unsafe的compareAndSwapInt方法,这个方法在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

    /**
    * 如果Java变量的当前值是expected,则原子性的把该变量的值更新为x。
    * @return 成功返回true
    */
    public final native boolean compareAndSwapInt(Object o, long offset,int expected, int x);
    
    

    用文字叙述一下getAndAddInt这个过程。我们假设现在只有两个线程,一个是主线程,一个A线程

    1. 在主线程第1次进入 do while循环,执行 v = getIntVolatile(o, offset);获取到的AtomicInteger的value是0赋值给v。

    2. 执行compareAndSwapInt(o, offset, v, v + delta);如果此时A线程没有修改AtomicInteger的value,那么获取AtomicInteger对象在内存中偏移量为offset处的value和v相等,执行成功,将AtomicInteger的value更新为1,返回true,循环结束。然后返回AtomicInteger的更新之前的value,就是0。

    3. 执行compareAndSwapInt(o, offset, v, v + delta);如果此时A线程修改了AtomicInteger的value(value值为1),
      那么获取AtomicInteger对象在内存中偏移量为offset处的值和v已经不相等了(内存中的值已经不变成1了,而v是0),执行失败,进入下一次循环。

    4. 在主线程第2次进入 do while循环,执行 v = getIntVolatile(o, offset);,因为A线程修改了AtomicInteger的value,获取到的AtomicInteger的value是1赋值给v。

    5. 执行compareAndSwapInt(o, offset, v, v + delta);如果此时A线程没有修改AtomicInteger的value,那么获取AtomicInteger对象在内存中偏移量为offset处的值和v相等(都是1),执行成功,将AtomicInteger的value更新为2,返回true,循环结束。然后返回AtomicInteger的更新之前的value,就是1。

    上面的循环可以保证,在不同的线程更新value值不会出现被覆盖的情况。也就是说实现了原子性操作。

    CAS 的问题

    1. ABA问题

    CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是CAS的ABA问题。

    常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    1. 循环时间长开销大
      上面我们说过如果CAS不成功,则会原地自旋(一直 do while),如果长时间自旋会给CPU带来非常大的执行开销。

    2. 只能保证一个共享变量的原子操作。
      对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。

    Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

    参考链接:

    1. Java CAS 原理剖析
    2. Unsafe.java
    3. 不可不说的Java“锁”事

    相关文章

      网友评论

          本文标题:关于Java中CompareAndSwap(CAS)的一些理解

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