参考网页
主要参考
★https://www.jianshu.com/p/fb6e91b013cc
http://zl198751.iteye.com/blog/1848575
https://www.xilidou.com/2018/02/01/java-cas/
其他参考
https://my.oschina.net/huangcongmin12/blog/692907
http://www.cnblogs.com/dayhand/p/3713303.html
https://my.oschina.net/vshcxl/blog/795495
https://blog.csdn.net/chenssy/article/details/69640293
https://mritd.me/2017/02/06/java-cas/
CAS是什么
CAS: 全称Compare and swap,字面意思:“比较并交换”,一个 CAS 涉及到以下操作:
假设内存中的原数据V,旧的预期值A,需要修改的新值B
- 比较 A 与 V 是否相等。(比较)
- 如果比较相等,将 B 写入 V。(交换)
- 返回操作是否成功。
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。
为何说当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功?多个线程访问,只有一次能成功的进行CAS操作(因为只有一个线程luck thread可以成功的捕捉到A和V相等的那一刻,然后其他线程访问时A和V肯定已经不相等了)。
CAS算法原理的一个描述
CAS 算法大致原理是:在对变量进行计算之前(如 ++ 操作),首先读取原变量值,称为 旧的预期值 A,然后在更新之前再获取当前内存中的值,称为 当前内存值 V,如果 A==V 则说明变量从未被其他线程修改过,此时将会写入新值 B,如果 A!=V 则说明变量已经被其他线程修改过,当前线程应当什么也不做。
CAS和synchronized
CAS和synchronized的区别
CAS是乐观锁,synchronized(重量级锁)是悲观锁。
Java技术发展史先设计出的synchronized,synchronized会产生阻塞问题,后来又发展出了CAS。
阅读JDK的源码可知,J.U.C包实际上是建立在CAS操作基础上的。ReentrantLock这些类的底层其实就采用的CAS操作。
CAS和synchronized没有绝对的好坏,各有各自适用的场景
可以用CAS在无锁的情况下实现原子操作,但要明确应用场合,非常简单的操作且又不想引入锁可以考虑使用CAS操作,当想要非阻塞地完成某一操作也可以考虑CAS。不推荐在复杂操作中引入CAS,会使程序可读性变差,且难以测试,同时会出现ABA问题。
不想引入锁或者想非阻塞的完成并发操作可以考虑使用CAS操作。但是CAS操作的代码可读性差难测试,复杂操作最好不用引入CAS。
★CAS实现原理:硬件层级的原子操作--AtomInteger为例进行分析
AtomicInteger -> sum.misc.Unsafe
AtomicInteger核心代码
public class AtomicInteger extends Number implements java.io.Serializable {
// 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;
public final int get() {
return value;
}
}
代码分析
1.Unsafe,是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。
2.变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
3.变量value用volatile修饰,保证了多线程之间的内存可见性。
AtomicInteger如何实现并发下的累加操作
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
上述代码已经使用到了Unsafe的方法。
sum.misc.Unsafe -> native(JNI方法)
unsafe.getAndAddInt
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;
}
代码分析
假设线程A和线程B同时执行getAndAdd操作(分别跑在不同CPU上):
- AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据Java内存模型,线程A和线程B各自持有一份value的副本,值为3。
- 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
- 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,运气好,线程B没有被挂起,并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为2。
- 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值(3)和内存的值(2)不一致,说明该值已经被其它线程提前修改过了,那只能重新来一遍了。
- 重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
整个过程中,利用CAS保证了对于value的修改的并发安全,继续深入看看Unsafe类中的compareAndSwapInt方法实现。
public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中。
native(JNI方法)-> unsafe.cpp
unsafe.cpp
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
代码分析
先想办法拿到变量value在内存中的地址。
通过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值。
unsafe.cpp -> Atomic::cmpxchg
如果是Linux的x86,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表示禁止编译器优化
LOCK_IF_MP是个内联函数
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
Window的x86实现如下
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::isMP(); //判断是否是多处理器
_asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
// 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:
代码分析
LOCK_IF_MP根据当前系统是否为多核处理器决定是否为cmpxchg指令添加lock前缀。
1.如果是多处理器,为cmpxchg指令添加lock前缀。
2.反之,就省略lock前缀。(单处理器会不需要lock前缀提供的内存屏障效果)
Atomic::cmpxchg -> lock前缀
intel手册对lock前缀的说明如下:
1.确保后续指令执行的原子性。
在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。
2.禁止该指令与前面和后面的读写指令重排序。
3.把写缓冲区的所有数据刷新到内存中。
上面的第2点和第3点所具有的内存屏障效果,保证了CAS同时具有volatile读和volatile写的内存语义。
总结:AtomicInteger -> sum.misc.Unsafe -> native(JNI方法)-> unsafe.cpp -> Atomic::cmpxchg -> lock前缀
★总结一下 JAVA 的 cas 是怎么实现的
java 的 cas 利用的的是 unsafe 这个类提供的 cas 操作。
unsafe 的cas 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg
Atomic::cmpxchg 的实现使用了汇编的 cas 操作,并使用 cpu 硬件提供的 lock信号保证其原子性
CAS的缺点
- ABA
- 循环时间长开销大
- 只能保证一个共享变量的原子操作