Java内存模型JMM之七CAS机制

术语

在上一文关于synchronized锁机制的探寻中,我们知道在那些锁机制的底层实现中或多或少的都借助了CAS操作,其实Java中java.util.concurrent包的实现也是差不多建立在CAS之上,可见CAS在Java同步领域的重要性。

 

CAS是Compare and Swap的简写形式,可翻译为:比较并交换。用于在硬件层面上提供原子性操作。其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用。比较是否和给定的数值一致,如果一致则修改,不一致则不修改。

 

CAS案例分析

AtomicInteger的原子特性就是CAS机制的典型使用场景。 其相关的源码片段如下(以下代码基于JDK1.7 以及openJDK7):

private volatile int value;

public final int get() {
        return value;
}

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

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

 

 AtomicInteger在没有锁的机制下借助volatile原语,保证了线程间的数据是可见的(共享的)。是get()方法可以获取最新的内存中的值。

在++1的操作中,使用了CAS操作,每次从内存中读取最新的数据然后将此数据+1,最终写入内存时,先比较内存中最新的值,同累加之前读出来的值是否一致,不一致则写失败,循环重试直到成功为止。

 

compareAndSet的具体实现调用了 unsafe类的compareAndSwapInt方法,它其实是一个Java Native Interface(简称JNI)java本地方法,会根据不同的JDK环境调用不同平台的对应C实现,下面以windows操作系统,X86处理器的实现为例,这个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomic_windows_x86.inline.hpp,它的实现代码存在于:openjdk7\hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp,下面是相关的代码片段:

 

// 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
  }
}

 

 由上面源代码可见在该平台的处理器上CAS通过指令cmpxchg实现,并且程序会根据当前处理器是否是多处理器(is_MP)来决定是否为cmpxchg指令添加lock前缀(LOCK_IF_MP),如果是单核处理器则省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。关于Lock前缀指令:

1. Lock前缀指令可以通过对总线或者处理器内部缓存加锁,使得其他处理器无法读写该指令要访问的内存区域,因此能保存指令执行的原子性。

2. Locl前缀指令将禁止该指令与之前和之后的读和写指令重排序。

3. Lock前缀指令将会把写缓冲区中的所有数据立即刷新到主内存中。

通过以上分析,我们深入CAS内部实现结合具体平台实现,就知道了CAS到底是如何保证操作的原子性了。

 

关于总线锁定和缓存锁定 写道
1、早期的处理器只支持通过总线锁保证原子性。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。很显然,这会带来昂贵的开销。
2、缓存锁定是改进后的方案。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。

但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定

  

CAS缺陷 

1.  ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。 

3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。 

4. 总线风暴带来的本地延迟。在上一章偏向锁的介绍中,我们提到CAS指令存在本地延迟,那么到底是指什么呢?我们知道多处理架构中,所有处理器会共享一条总线,靠此总线连接主存,每个处理器核心都有自己的高速缓存,各核相对于BUS对称分布,这种结构称为“对称多处理器”即SMP。当主存中的数据同时存在于多个处理器高速缓存的时候,某一个处理器的高速缓存中相应的数据更新之后,会通过总线使其它处理器的高速缓存中相应的数据失效,从而使其重新通过总线从主存中加载最新的数据,大家通过总线的来回通信称为“Cache一致性流量”,因为总线被设计为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈。而CAS恰好会导致Cache一致性流量,如果有很多线程都共享同一个对象,当某个核心CAS成功时必然会引起总线风暴,这就是所谓的本地延迟。而偏向锁就是为了消除CAS,降低Cache一致性流量。

 

 

例外之言 写道
其实也不是所有的CAS都会导致总线风暴,这跟Cache一致性协议有关,具体参考:http://blogs.oracle.com/dave/entry/biased_locking_in_hotspot

NUMA(Non Uniform Memory Access Achitecture)架构:
与SMP对应还有非对称多处理器架构,现在主要应用在一些高端处理器上,主要特点是没有总线,没有公用主存,每个Core有自己的内存

猜你喜欢

转载自pzh9527.iteye.com/blog/2414497