Java并发——CAS

什么是CAS?

  CAS是Compare And Swap的简称。在Java中有很多实现,比如compareAndSwapObject()方法,或者compareAndSwapInt()方法等。多用在包java.util.concurrent.atomic下的类中来实现原子性的操作。这里主要是结合compareAndSwapInt()方法介绍一下CAS的核心思想以及一些问题。

什么是原子性?

  对于原子,相信学过化学的都知道,化学反应到最后不可分割的是原子。如果把我们的系统各个线程比作各种各样的化学方程式的话,其中有一部分我们就希望无论系统怎么运行,这部分都是像原子一样不能再被分割。前面说过synchronized和CAS可以实现原子性,而volatile不可以,从使用的方法上我们也可以看出,synchronized修饰的部分都是代码块或方法,也就是范围大,可以理解里面的部分都是原子的组成部分,同理CAS实际上也是一个方法,而volatile只是修饰一个变量。要实现原子性也很简单,只需要保证每次只有一个线程来运行这里面的代码即可,像我们刚接触Java时只写了主线程,那么主线程中的代码就是原子性的。上次介绍了synchronized关键字原理,现在就看看CAS在底层到底是怎么实现的。

++运算符和getAndIncreament()方法的线程安全性比较

  在平常相信每个人都接触过for循环

for(int i=0;i<10;i++);

  上面这段代码大家肯定都敲过很多遍。对于i++运算,其实表面看起来是i进行自增加,但其实在底层i++操作是分三步:

    1、从主存中取出i的值

    2、i进行加一操作

    3、把值写回主存

  这三步操作还是i是被volatile修饰的情况下才会进行,而平常更多的是直接操作线程副本里的i。当i是共享变量时在多个线程的情况下就会出现错误。比如线程A修改了i的值却没有写到内存,其他线程拿i的旧值进行操作,或者是线程A和线程B同时进行上面三步,那么原本是要加两次的,实际上就只加了一次。所以这是不安全的。而如果我们使用AtomicInteger类型中getAndIncreament()方法就是线程安全的,下面来分析一下这个方法为什么线程安全

public class AtomicInteger extends Number implements java.io.Serializable {
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
}

  我们看见这个方法中调用了unsafe.getAndAddInt方法并且传了三个参数,这三个参数分别是

    this:当前的AtomicInteger类的对象。

    valueOffset:可以理解为value在内存中的地址。也就是要从内存中获取最新的value值。

    1:这个就很好理解了,就是每次需要增加的数,也就是自增1。  

  然后进入unsafe.getAndAddInt方法中

public final class Unsafe {
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;
    }
}

  说一下里面的大概实现的功能。首先定义了var5,var5是通过getIntVolatile方法获取当前内存中AtomicInteger当前对象的最新值,然后比较当前对象的值,也就是var1中的value属性值和我重新在主内存中拿的值var5对应的value值是否一样,一样才会进行加一操作,并且退出while循环,最后返回var5。如果不一样就会一直从底层中去取,直到取的是和当前对象的值一样了才会执行后面的操作。现在看到这其实已经发现很多CAS存在的问题了。

  1、ABA问题

    上面说的比较当前值和内存中的值一样怎么即可进行下次操作。那么如果有其他线程在改过这个变量的值之后,又因为某些操作而把变量的值改回来,这时候比较的值一样了,CAS就会进行相应的操作。通俗一点说就是现在线程1需要拿共享变量的A值进行加一操作,而线程2把共享变量的值改成B值,之后又把这个变量改回A值,那么在同一时刻实际上有两个线程操作了这个值,所以破坏了原子性。这个问题解决思路就是加上版本号,对这个变量改过一次版本号就更新一次。通过版本号来记录变量的改动次数。在JDK1.5后Atomic包中就提供了一个类AtomicStampedReference来解决ABA问题。这里说一下compareAndSet方法具体的步骤是首先检查引用是否等于预期引用,也就是在内存中拿的值是否等于预期操作的值,然后检查当前的标志是否等于预期的标志,这里可以理解为版本号。

  2、循环时间开销大

    如果从内存中拿到的值一直和预期的值不相等,那么就会一直循环一直拿。这样会给CPU带来很大的消耗,这里稍微提一下,JVM支持处理器提供的pause指令时,这个问题才会得到一定的改善。

  3、原子性不互相兼容

    这个其实是说AtomicInteger类下的单独一个方法是原子性的,但是两个方法之间放在一起操作这个对象就不是原子性的,或者对多个共享变量操作时,CAS也无法保证操作是原子性。这个可以通过锁来实现(也就是需要使用一个对象下的多个方法一起进行操作的时候使用)或者通过AtomicReference类来保证引用对象之间的原子性(内部是通过把多个变量放在一个对象里来进行CAS操作)

总结

  其实说到这,CAS只是其中的一个方法,而Atomic包下的类才是我们使用的主角,只不过底层在保证原子性的时候使用的是CAS方法+volatile变量,volatile变量是类中用来修饰value的,所以value会在每次操作的时候都会和主内存进行交互。CAS负责操作这个value值。CAS具体的实现是在用C++代码编写的,这里就没继续去看对应的源代码。若需要学习,完全可以去看看底层如何通过C++来实现的。

posted @ 2019-04-27 10:57  方块人  阅读(580)  评论(0编辑  收藏  举报