CAS的全称是CompareAndSwap,比较并交换,是Java保证原子性的一种重要方法,也是一种乐观锁的实现方式。
它需要先提前一步获取旧值,然后进入此方法比较当下的值是否与旧值相同,如果相同,则更新数据,否则退出方法,重复一遍刚才的动作。由此可见,CAS方法是非堵塞的。CAS方法需要三个参数,变量内存值、旧的预期值、数据更新值
CAS的伪代码可以表示为:
do{
获取备份旧数据;
准备更新的数据;
}while( !CAS( 内存地址,备份的旧数据,新数据 ))
private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset;
/** * Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. * * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that * the actual value was not equal to the expected value. */ public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
上面是原子类AtomicInteger的局部源码,可以看出来Java中的CAS操作都是由Sun包下的Unsafe类实现的,而unsafe类中的方法都是Native方法,都是由JVM本地实现。
然后简单去网上找了找相关的openJDK源码,它调用了Atomic:comxchg方法,这个方法的实现放在了hotspot下的os_cpu包中的,也就是说它调用了操作系统和CPU层级的东西。
经过进一步了解获知,这个方法在Linux x86系统的多处理器情况下是通过调用LOCK指令来实现的,立刻写会内存且令其他缓存行失效。
CAS的问题:
1、ABA问题,比如刚开始读取到备份是3,然后被其他线程连续修改两次,最终结果还是3,那么CAS很可能识别不到数据发生了改变,这种情况对程序造成了极大的安全隐患。可以通过添加版本号等标志位来解决该问题。
2、循环时间长开销大,如果长时间自旋不成功,会给CPU带来很大开销。可以使用自适应自旋锁解决这个问题
3、只能保证一个共享变量的原子操作。比如AtomicInteger都是每次只能对一个变量进行原子性控制。
单次CAS操作的开销:
这是一个8核CPU的计算机系统,先简单介绍一下CPU的硬件结构体系。每个CPU都有一个高速缓存区Cache(寄存器),每两个CPU之间有一段互联模块可以让管芯内的两个核可以互相通信,最中间还有个系统互联模块可以让四个管芯进行通讯。数据以“缓存线”为单位在系统中传输,缓存线对应内存中2的整数幂的一个字节块
计算机系统中,当CPU要获取一个变量值的时候,必须要将包含了该变量的缓存线保存到自己的寄存器中;CPU要向主存写入值的时候,根据缓存一致性协议,必须保证其他CPU获知此操作,或者让其他CPU删除该缓存线的拷贝。
因此,如果CPU1和CPU5同时持有一个变量的缓存线,然后CPU5想进行CAS操作,那么它需要访问CPU5、6的互联模块,未找到持有该缓存线的CPU,然后访问系统级的互联模块,发现第一处互联模块有该缓存线,继而访问第一处互联模块,最终找到了CPU1。此时才算是完成了大部分操作。因此如果从最优角度考虑,CAS所花费的时钟周期较锁来说要小很多。
在Java中,sun.misc.Unsafe类提供了硬件级别的院子操作来实现这个CAS,JUC包下的大量类都使用了这个Unsafe.java类的CAS操作。