Java并发学习(六)-深入分析CAS操作

What is CAS

在Java并发包下源码中,经常会遇到CAS操作,即Compare And Swap操作,例如:

    //AQS里面替换head
    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }
     //替换tail
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

   //设置waitStatus
    private static final boolean compareAndSetWaitStatus(Node node,int expect,int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
    }

如上,了解AQS(AbstractQueuedSynchronizer)的童鞋就知道,不清楚AQS的,可以参看:Java并发学习(三)-AbstractQueuedSynchronizer,上面几个操作,都是存在竞争的,都是会被类似于for(;;)的阻塞方法中调用。反过来想,由于竞争才被调用,所以很明显,上诉CAS过程必须是原子性的。

从前一片博文可以知道,Unsafe类其实调用的是底层JNI方法的c++函数,可以看:Java并发学习(四)-sun.misc.Unsafe

那么现在要考虑的,就是众多CAS又是怎么一种实现思路。

CAS实现

记得在讲synchronize的时候,提到了多cpu下如何保证数据一致性的问题,主要有两种方法,

  • 一种是在总线上加#LOCK信号而锁住总线
  • 另一种利用缓存一致性协议来保证原子性。

总线加锁:总线加锁就是就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
缓存加锁:其实针对于上面那种情况只需要保证在同一时间对某个内存地址的操作是原子性的即可。缓存加锁就是缓存在内存区域的数据如果在加锁期间,当它执行锁操作写回内存时,处理器不再利用LOCK#信号,而是修改内部的内存地址,利用缓存一致性协议来保证原子性。缓存一致性机制可以保证同一个内存区域的数据仅能被一个处理器修改,也就是说当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行,此时CPU2可以去做其他的事。

现在看CAS相关操作具体实现代码:

CAS通过调用JNI的代码实现的。JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言程序。而compareAndSwapInt就是借助C++来调用CPU底层指令实现的。由于各种cpu的架构不同,指令也有差异,主要有x86,zero,ppc,sparc等几种架构,并且随着操作系统不同,指令之间也会有些许差异,这里以linux下x86架构下代码为例。

Unsafe.java

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/07011844584f/src/share/classes/sun/misc/Unsafe.java
下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:

public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);
unsafe.cpp

所调用的本地方法会继而调用到hotspot里面的unsafe.cpp程序。
http://hg.openjdk.java.net/jdk8u/jdk8u20/hotspot/file/190899198332/src/share/vm/prims/unsafe.cpp

下面是Unsafe_CompareAndSwapInt函数:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
atomic.cpp

如上,获取到相应变量和指针后,就直接调用了Atomic的cmpxchg函数,位置在:
http://hg.openjdk.java.net/jdk8u/jdk8u20/hotspot/file/55fb97c4c58d/src/share/vm/runtime/atomic.cpp

里面cmpxchg函数代码如下:

jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest, jbyte compare_value) {
  assert(sizeof(jbyte) == 1, "assumption.");
  uintptr_t dest_addr = (uintptr_t)dest;
  uintptr_t offset = dest_addr % sizeof(jint);
  volatile jint* dest_int = (volatile jint*)(dest_addr - offset);
  jint cur = *dest_int;
  jbyte* cur_as_bytes = (jbyte*)(&cur);
  jint new_val = cur;
  jbyte* new_val_as_bytes = (jbyte*)(&new_val);
  new_val_as_bytes[offset] = exchange_value;
  while (cur_as_bytes[offset] == compare_value) {
    jint res = cmpxchg(new_val, dest_int, cur);
    if (res == cur) break;
    cur = res;
    new_val = cur;
    new_val_as_bytes[offset] = exchange_value;
  }
  return cur_as_bytes[offset];
}

从这里面我们可以看出,开始以为CAS也是一种自旋的方式,后来自己也疑惑了好久,后来就去StackOverflow上问了下,感觉就理解多了。
https://stackoverflow.com/questions/47569600/is-cas-a-loop-like-spin
主要意思就是,在x86上和SPARC架构的处理器上,CAS在intlong层面操作时,对intlong操作是一条单一的指令操作,分别对应指令为cmpxchglcmpxchgq,是原子性的。但是上述代码的Atomic::cmpxchg 则是在字节层面上的操作。所以需要循环来查看4个字节是否都一致。

atomic_linux_x86.inline.hpp

最终,还是会调用到相应的汇编语言代码,文件在:
http://hg.openjdk.java.net/jdk8u/jdk8u20/hotspot/file/f06c7b654d63/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp

其中,Atomic::cmpxchg的内联函数为:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

其中,__asm__ volatile为GCC在C语言中内嵌汇编的写法。
如上面源代码所示,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,可以类比as-if-serial语义理解,不需要lock前缀提供的内存屏障效果)。

CAS的不足之处

虽然Java里面并发操作大多数都是用到CAS+volatile的方法,利用一个volatile变量来判断,但是就CAS本身来说,还是有不足之处。

循环时间太长

如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。当然,在concurrent包下也有通过类似于syncrhonized限制自旋次数的工具类,例如SynchronousQueue。

ABA问题

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
**从Java1.5开始JDK的atomic包里提供了一个类**AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

只能保证一个共享变量的原子操作

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

other

另外,由于本文是基于Java8分析的,而在Unsafe类CAS上,Java7与Java8是有区别的,Java8对相关CAS代码进行了优化,把自旋相关代码也都放入了native方法里面了,可以阅读:http://ifeve.com/enhanced-cas-in-jdk8/

CAS等相关类使用场景

很明显,如上文分析,如果一直修改不成功,就会一直占用cpu消耗大量资源,所以有下总结:

  • 使用CAS在线程冲突严重时,会大幅降低程序性能;CAS只适合于线程冲突较少的情况使用。
  • synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

最后借用一张图来展示concurrent包下面类的架构:

这里写图片描述

参考资料:
http://www.infoq.com/cn/articles/atomic-operation

http://blog.csdn.net/pbymw8iwm/article/details/8227839

https://www.cnblogs.com/everSeeker/p/5569414.html

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值