浅谈cas

前言

研究java并发编程有一段时间了, 在并发编程中cas出现的次数极为频繁。cas的英文全名叫作compare and swap,意思很简单就是比较并交换。在jdk的conurrent包中,Doug Lea大神大量使用此技术,实现了多线程的安全性。
cas的核心思想就是获取当前的内存偏移值、指望值和更新值,若是根据内存偏移值获得的变量等于指望值,则进行更新。java

问题

总有面试官喜欢问你i++和++i,以及经典的字符串问题,其实这些问题只要你试用javap -c这个命令反编译一下,就一目了然。固然今天的主题是cas,我首先来研究下a++:c++

//@RunWith(SpringRunner.class)
//@SpringBootTest
public class SblearnApplicationTests {

    public static volatile  int a;
    public static void main(String[] args) {
        a++;
    }

}

经过javac SblearnApplicationTests.java,javap -c SblearnApplicationTests.class能够获得:面试

Compiled from "SblearnApplicationTests.java"
public class com.example.sblearn.SblearnApplicationTests {
  public static volatile int a;

  public com.example.sblearn.SblearnApplicationTests();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field a:I
       3: iconst_1                          //当int取值-1~5采用iconst指令,取值-128~127采用bipush指令,取值-32768~32767采用sipush指令,取值-2147483648~2147483647采用 ldc 指令
       4: iadd
       5: putstatic     #2                  // Field a:I
       8: return
}

经过反编译得出如上的结果,都是一些jvm的指令,百度一下就能知道意思。咱们将变量a用violate修饰,保证线程间的可见性。经过jvm指令可知a++不是一个原子动做,若是多个线程同事对a进行操做,没法保证线程安全,那怎么解决呢?编程

解决方案

java给咱们提供了一个关键字synchronized,能够对成员方法、静态方法、代码块进行加锁,从而保证操做的原子性。但效率不高,还有其余办法吗?固然有了,就是咱们今天的主角cas。接下来咱们再来看看concurrent包下的AtomicInteger:windows

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;
    //效果等同于a++,但保证了原子性
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    }
public final class Unsafe {

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

    public native int getIntVolatile(Object var1, long var2);
    //object var1:当前AtomicInteger对象,long var2Integer对象的内存偏移值,int var4 增长的值
    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机制的核心类就是Unsafe,valueOffset 是其内存偏移值。因为java语言没法直接操做底层,须要本地方法(native method)来访问,unsafe这个类中存在大量本地方法,就是在调用c去操做特定内存的数据。咱们先假设unsafe帮咱们保证了原子性,先来分析下AtomicInteger.getAndIncrement(),在jdk1.8中,其实现就是Unsafe.getAndAddInt()缓存

  1. 如今咱们假设有A、B线程同时来操做AtomicInteger,其初始值为1,根据java内存模型,当前主内存AtomicInteger值为1,线程A、线程B各自的工做内存也为1.
  2. 线程A得到经过getIntVolatile获取当前值,被挂起。线程B也经过此方法获取当前值,进行操做,比较内存值相等进行修改。
  3. 这时线程A恢复,执行compareAndSwapInt发现与内存指望值不相等,从新获取var5变量(由于被violate修饰,因此工做内存和主内存变量一致),再次比较与内存指望值相等,进行更新。

咱们经过cas保证了对value的并发线程安全,其安全的保证是CAS经过调用JNI的代码实现的。JNI:Java Native Interface为JAVA本地调用,容许java调用其余语言。而compareAndSwapInt就是借助C来调用CPU底层指令实现的。下面从分析比较经常使用的CPU(intel x86)来解释CAS的实现原理。compareAndSwapInt方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。这个本地方法的最终实如今openjdk的以下位置:openjdk-7-fcs-src-b147-27jun2011openjdkhotspotsrcoscpuwindowsx86vm atomicwindowsx86.inline.hpp(对应于windows操做系统,X86处理器)。下面是对应于intel x86处理器的源代码的片断:安全

// Adding a lock prefix to an instruction on MP machine  
// VC++ doesn't like the lock prefix to be on a single line  
// so we can't insert a label after the lock prefix.  
// By emitting a lock prefix, we can define a label after it.  
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \  
                       __asm je L0      \  
                       __asm _emit 0xF0 \  
                       __asm L0:  
  
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {  
  // alternative for InterlockedCompareExchange  
  int mp = os::is_MP();  
  __asm {  
    mov edx, dest  
    mov ecx, exchange_value  
    mov eax, compare_value  
    LOCK_IF_MP(mp)  
    cmpxchg dword ptr [edx], ecx  
  }  
}

如上面源代码所示,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。若是程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,若是程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不须要lock前缀提供的内存屏障效果)。多线程

intel的手册对lock前缀的说明以下:并发

1.确保对内存的读-改-写操做原子执行。在Pentium及Pentium以前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其余处理器暂时没法经过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上作了一个颇有意义的优化:若是要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),而且该内存区域被彻底包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。因为在指令执行期间该缓存行会一直被锁定,其它处理器没法读/写该指令要访问的内存区域,所以能保证指令执行的原子性。这个操做过程叫作缓存锁定(cache locking),缓存锁定将大大下降lock前缀指令的执行开销,可是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
2.禁止该指令与以前和以后的读和写指令重排序。
3.把写缓冲区中的全部数据刷新到内存中。jvm

cas的缺点

cas的缺点就是会出现aba问题,假如一个字母为a,它经历a->b->a的过程,实际已经改变两次,但值相同。部分业务场景是不容许出现这种状况的(好比银行转帐..).解决办法就是添加版本号,他就变成了1a->2b>3a。jdk1.5以后也提供了AtomicStampedReference来解决aba问题。

总结

自旋cas若是长时间不成功,将会对cpu带来很是大的开销。cas只能保证一个共享变量的原子操做。因此很是简单的操做又不想引入锁,cas是一个很是好的选择。

相关文章
相关标签/搜索