CAS的全称为Compare And Swap,直译就是比较交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在intel的CPU中,使用的是cmpxchg
指令,就是说CAS是靠硬件实现的,从而在硬件层面提升效率。
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,其它原子操作都是利用类似的特性完成的。 在 java.util.concurrent
下面的源码中,Atomic
, ReentrantLock
都使用了Unsafe类中的方法来保证并发的安全性。
CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁,JDK中大量使用了CAS来更新数据而防止加锁来保持原子更新。
CAS 操作包含三个操作数 :内存偏移量位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
下面来看一下 java.util.concurrent.atomic.AtomicInteger.java
,getAndIncrement()
,getAndDecrement()
是如何利用CAS实现原子性操作的。
valueOffset
字段表示"value"
内存位置,在compareAndSwap
方法 ,第二个参数会用到.
关于偏移量
Unsafe
调用C 语言可以通过偏移量对变量进行操作
实现逻辑封装在 Unsafe 中 getAndAddInt
方法,继续往下看,Unsafe
源码解析
在JDK8中追踪可见sun.misc.Unsafe
这个类是无法看见源码的,打开openjdk8
源码看
文件:openjdk-8-src-b132-03_mar_2014.zip
目录:openjdk\jdk\src\share\classes\sun\misc\Unsafe.java
通常我们最好也不要使用Unsafe
类,除非有明确的目的,并且也要对它有深入的了解才行。要想使用Unsafe
类需要用一些比较tricky
的办法。Unsafe类使用了单例模式,需要通过一个静态方法getUnsafe()
来获取。但Unsafe类做了限制,如果是普通的调用的话,它会抛出一个SecurityException
异常;只有由主类加载器加载的类才能调用这个方法。
下面是sun.misc.Unsafe.java
类源码
获取到Unsafe实例之后,我们就可以为所欲为了。Unsafe类提供了以下这些功能:
https://www.cnblogs.com/pkufork/p/java_unsafe.html
利用 Unsafe 类的 JNI compareAndSwapInt 方法实现,使用CAS实现一个原子操作更新,
compareAndSwapInt 四个参数:
1、当前的实例 2、实例变量的内存地址偏移量 3、预期的旧值 4、要更新的值
上面的分析看起来比较多,不过主流程并不复杂。如果不纠结于代码细节,还是比较容易看懂的。接下来,我会分析 Windows 平台下的 Atomic::cmpxchg 函数。继续往下看吧。
上面的代码由 LOCK_IF_MP 预编译标识符和 cmpxchg 函数组成。为了看到更清楚一些,我们将 cmpxchg 函数中的 LOCK_IF_MP 替换为实际内容。如下:
到这里 CAS 的实现过程就讲了,CAS 的实现离不开处理器的支持。以上这么多代码,其实核心代码就是一条带 lock
前缀的 cmpxchg
指令,即lock cmpxchg dword ptr [edx], ecx
。
通过上述的分析,可以发现AtomicInteger
原子类的内部几乎是基于前面分析过Unsafe
类中的CAS
相关操作的方法实现的,这也同时证明AtomicInteger
getAndIncrement
自增操作实现过程,是基于无锁实现的。
假设这样一种场景,当第一个线程执行CAS(V,E,U)操作。在获取到当前变量V,准备修改为新值U前,另外两个线程已连续修改了两次变量V的值,使得该值又恢复为旧值,这样的话,我们就无法正确判断这个变量是否已被修改过,如下图:
这就是典型的CAS的ABA问题,一般情况这种情况发现的概率比较小,可能发生了也不会造成什么问题,比如说我们对某个做加减法,不关心数字的过程,那么发生ABA问题也没啥关系。但是在某些情况下还是需要防止的,那么该如何解决呢?在Java中解决ABA问题,我们可以使用以下原子类
AtomicStampedReference类
AtomicStampedReference原子类是一个带有时间戳的对象引用,在每次修改后,AtomicStampedReference不仅会设置新值而且还会记录更改的时间。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值才能写入成功,这也就解决了反复读写时,无法预知值是否已被修改的窘境
底层实现为: 通过Pair私有内部类存储数据和时间戳, 并构造volatile修饰的私有实例
接着看 java.util.concurrent.atomic.AtomicStampedReference
类的compareAndSet()方法的实现:
同时对当前数据和当前时间进行比较,只有两者都相等是才会执行casPair()方法,
单从该方法的名称就可知是一个CAS方法,最终调用的还是Unsafe
类中的compareAndSwapObject
方法
到这我们就很清晰AtomicStampedReference
的内部实现思想了,
通过一个键值对Pair
存储数据和时间戳,在更新时对数据和时间戳进行比较,
只有两者都符合预期才会调用Unsafe
的compareAndSwapObject
方法执行数值和时间戳替换,也就避免了ABA的问题。
期望 Pair<V> cmp(A) == 当前内存存偏移量位置 Pair(V),就更新值 Pair<V> val(B)成功返回true 否则 false
通过以上原子更新方法,可见 AtomicStampedReference就是利用了Unsafe的CAS方法+Volatile关键字对存储实际的引用变量和int的版本号的Pair实例进行更新。
END