原本是准备阅读 j.u.c 包下 ConcurrentHashMap 的底层源码,理解 ConcurrentHashMap 的实现原理的,看了一点点发现里面用到了不少 CAS。而且 atomic 和 locks 这两个包中也大量使用了 CAS,因此就先把 CAS 的原理搞清楚了以后再继续后面的内容。html
看了一大堆文章,也是把它弄懂了。令我没想到的是,本身居然从 Java 源码看到 openjdk 源码及汇编码,最后还看了一些 intel 手册的内容,最终不只学会了 CAS,还学到了许多其余的知识。java
慢慢发现,其实深刻研究某一个知识点的实现,仍是蛮有意思的,只是过程可能有点艰辛。linux
CAS 是乐观锁的一种实现方式,是一种轻量级锁,j.u.c 中不少工具类的实现就是基于 CAS 的。c++
CAS (Compare And Swap)比较并交换操做。git
CAS 有 3 个操做数,分别是内存位置 V、旧的预期值 A 和拟修改的新值 B。当且仅当 V 符合预期值 A 时,用新值 B 更新 V 的值,不然什么都不作。github
用一段伪代码来帮助理解 CAS:windows
Object A = getValueFromV();// 先读取内存位置 V 处的值 A
Object B = A + balaba;// 对 A 作必定处理,获得新值 B
// 下面这部分就是 CAS,经过硬件指令实现
if( A == actualValueAtV ) {// actualValueAtV 为执行当前原子操做时内存位置 V 处的值
setNewValueToV(B);// 将新值 B 更新到内存位置 V 处
} else {
doNothing();// 说明有其余线程改过内存位置 V 处的值了,A 已经不是最新值了,因此基于 A 处理获得的新值 B 是不对的
}
复制代码
CAS 的核心是 Unsafe 类。而当你去看 Unsafe 的源码的时候,发现里面调用的是 native 方法。而要看 native 方法的实现,确实须要花很大一番功夫,而且基本上都是 C++ 代码。缓存
在通过一番折腾后,至少我大体知道了 Unsafe 类中的 native 方法的调用链及关键的 C++ 源码。数据结构
以 compareAndSwapInt
为例,这个本地方法在 openjdk 中依次调用的 C++ 代码为:多线程
(1)unsafe.cpp
:openjdk/hotspot/src/share/vm/prims/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
复制代码
(2)atomic_****_****.inline.hpp
atomic_windows_x86.inline.hpp
:openjdk/hotspot/src/os_cpu/windows_x86/vm/atomic_windows_x86.inline.hpp
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
}
}
复制代码
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: "
复制代码
以上几个参考自占小狼的深刻浅出 CAS。
os::is_MP()
这个函数是判断当前系统是不是多核处理器。
因此这个地方应该就是生成汇编码,我就只关注了这一行 LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
,毕竟后面的也看不懂。。。
这里至关因而若是当前系统是多核处理器则会在添加 lock 指令前缀,不然就不加。
关于
lock
指令前缀说明:
- 在《深刻理解 Java 虚拟机》中,做者解释 volatile 的内存可见性原理时提到(371 页):“关键在于 lock 前缀,查询 IA32 手册,它的做用是使得本 CPU 的 Cache 写入了内存,该写入动做也会引发别的 CPU 或者别的内核无效化( Invalidate )其 Cache,这种操做至关于对 Cache 中的变量作了一次前面介绍 Java 内存模式中所说的 ' store 和 write ' 操做。”
- intel IA32 手册 中 8.1 LOCKED ATOMIC OPERATIONS 关于 lock 前缀的含义:
- 保证原子操做
- 总线锁定,经过使用 LOCK# 信号和 LOCK 指令前缀
- 高速缓存一致性协议,确保能够对高速缓存的数据结构执行原子操做(缓存锁定)
- lock 指令前缀也具备禁止指令重排序做用:能够经过阅读 intel IA32 手册中 8.2.2 Memory Ordering in P6 and More Recent Processor Families 和 8.2.3 Examples Illustrating the Memory-Ordering Principles 两节的内容得出。
看到这里,CAS 的底层实现原理也就很显然了,实际上就是:lock cmpxchg
其中,cmpxchg
是硬件级别的原子操做指令,lock
前缀保证这个指令执行结果的内存可见性和禁止指令的重排序。
关于
lock cmpxchg
的一些我的理解:因为lock
指令前缀会锁定总线(或者是缓存锁定),因此在该 CPU 执行时总线是处于独占状态,该 CPU 经过总线广播一条read invalidate
信息,经过高速缓存一致性协议(MESI),将其他 CPU 中该数据的 Cache 置为invalid
状态(若是存在该数据的 Cache ),从而得到了对该数据的独占权,以后再执行cmpxchg
原子操做指令修改该数据,完成对数据的修改。
volatile 的读和写的内存语义实际上是经过 lock 指令前缀实现的,如图:
而 CAS 在系统是多核处理器时也会添加 lock 指令前缀,这两个不就是重复了吗?
原本想经过工具查看 CAS 这一部分的汇编码的,不过 Java 代码的汇编码不包含这一部分,也就不知道这一部分的汇编码究竟是啥样子的,由于这部分是由 C++ 实现的。
这个做以下猜想:
lock
指令的,这种状况仅靠 cmpxchg
可否保证各个线程的本地缓存失效呢?对 volatile 变量作 CAS 是否能够避免这个问题?以上仅是我我的的一些理解,不必定正确,欢迎你们来一块儿讨论。
不加锁,在并发冲突程度不高的状况下,效率极高。(能够参考乐观锁的优势)
大多数状况下,CAS 是配合自旋来实现对单个共享变量的更新的。
若是自旋 CAS 长时间不成功(说明并发冲突大),会给 CPU 带来很是大的执行开销。
首先明白一点:CAS 自己是一个原子操做,不存在 ABA 问题。
不过使用 CAS 更新数据通常须要三个步骤:
在这个过程当中可能出现 ABA 问题。上面三个步骤不是一个原子操做,因此可能出现下面这种状况:
线程 thread1 在处理数据的过程当中,实际上 A 的值已经经历了 a -> b -> a
的过程,可是对于线程 thread1 来讲判断不出来,因此线程 thread1 仍是能够将 A 的值更新为 c。这就是咱们说的 ABA 问题。
这里我用 Java 代码模拟了一下 ABA 问题,有兴趣的能够去看一下:CasABAProblem
ABA 问题可能带来的问题是什么呢?换句话说,a -> b -> a
这个过程可能会有哪些反作用?
思考了好久,没想到什么好的例子。。。等想到了以后再来更新。。。下面咱们来看如何避免 ABA 问题。
其实避免 ABA 问题其实很简单,只须要给数据添加一个版本号。上面例子中的 a -> b -> a
的过程就会变成 1a -> 2b -> 3a
,当线程 thread1 处理完数据,发现 1a != 3a
,因此也就不会更新 A 的值了。能够参考 j.u.c atomic 包下 AtomicStampedReference 类,它就是添加了一个 stamp 字段做为数据的版本号。
我还试了一下 compareAndSwapObject 方法,发现这个方法比较的是对象的引用,由于无论我怎么修改对象中的属性,compareAndSwapObject 都能执行成功。。。因此 Unsafe 中 compareAndSwap 的 compare 是否就能够用 == 来等价呢?看了一下,AtomicReference 中 compareAndSet(V expect, V update) 上的文档好像也确实是这么写的:
* Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. 复制代码
我看到不少人的博客上面写了 ABA 问题,举了链表或栈的出栈入栈相关的例子。参考 wikipedia 上面的例子:比较并交换
里面是用 C 操做的堆,对堆进行了一系列的 pop 和 push 操做。并解释说:因为内存管理机制中普遍使用的内存重用机制,致使 NodeC 的地址与以前的 NodeA 一致。
这种状况在 Java 中会出现吗?我以为仍是能够思考思考的。
从上面的介绍来看,CAS 自己就是针对单个共享变量的,对于多个共享变量,固然是不支持的。
固然,若是把多个共享变量合并成一个共享变量(放在一个对象里面),也是能够进行 CAS 操做。
这就看怎么理解多个共享变量了,若是说一个共享变量的多个属性能够被称之为多个共享变量,那么 CAS 也是能够支持的。
学 CAS ,最后学到的知识有:
可以粗浅阅读一些 openjdk 的 C++ 源码
加深对 volatile 的理解
lock 指令的做用
内存屏障
如何反汇编 Java 字节码
以及一些工具的使用
收获颇丰!给本身点个赞!哈哈哈~
参考资料:
(1)深刻浅出CAS