美文网首页
关于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