注:本文是在http://www.toutiao.com/i6421671637946466817/?wxshare_count=2&pbid=1498719626基础上整理!
CAS:Compare and Swap,即比较再交换。是无锁操作,也有人说他是乐观锁,也许是相对于独占锁来说,原子性操作(通过操作系统指令来实现)。
重入锁:java.util.concurrent.ReentrantLock和AutomicInteger就是使用CAS算法实现的。它是通过算法的
来实现数据操作的互斥性。
CAS有3个操作数:内存值V(可能被thread修改)、旧的预期值A(thread修改后打算回写时看的主存变量值)、要修改的新值B(当前thread操作后的值)。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。
举例:
thread1,thread2线程是同时更新同一变量9的值
因为thread1和thread2线程都同时去访问同一变量9,所以这两个线程会把主内存的值完全拷贝一份到自己的工作内存空间,所以thread1和thread2线程的内存预期值都为9。假设thread1在与thread2线程竞争中,线程thread1能去更新变量的值,而其他线程都失败。
ps:失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试。重复CAS算法
thread1线程去更新变量值改为B:10,然后比较A:预期值9和内存的值C:9发现还没被改变,然后写到内存中。
此时对于thread2来说,预期值A:9,内存值变为了C:10,A≠C,就操作失败了.
通俗解释:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。
Linux GCC 支持的 CAS
GCC4.1+版本号中支持CAS的原子操作(完整的原子操作可參看 GCC Atomic Builtins)
1: bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
2: type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)
Windows支持的CAS
在Windows下。你能够使用以下的Windows API来完毕CAS:(完整的Windows原子操作可參看MSDN的InterLocked Functions)
1: InterlockedCompareExchange ( __inout LONG volatile *Target,
2: __in LONG Exchange,
3: __in LONG Comperand);
C++ 11支持的CAS
C++11中的STL中的atomic类的函数能够让你跨平台。(完整的C++11的原子操作可參看 Atomic Operation Library)
1: template< class T >
2: bool atomic_compare_exchange_weak( std::atomic<T>* obj,
3: T* expected, T desired );
4: template< class T >
5: bool atomic_compare_exchange_weak( volatile std::atomic<T>* obj,
6: T* expected, T desired );
ABA问题:
虽然CAS操作是原子性操作--比较并交换。不过它的实现过程是首先读取出内存中的值,我们称之为“读”,然后再进行运算,然后才是CAS指令操作,我们称之为“写”,【读+写】这个过程并不是原子性性的,当thread1和thread2同时读取到内存值v=1后,thread1进行+1操作,更新后的值B=2,预期值A=1,然后比较预期值A是否跟此时的内存值相同,相同则操作成功,否则重试。java并发包中的java.util.concurrent.atomic.AtomicInteger类就用到了CAS操作:
private volatile int value;
public final int get() {
return value;
}
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
this代表当前对象AutomicInteger,valueOffset代表value的内存地址,expect代表cas前读取到的value值,update表示操作后的更新值。通过compareAndSet方法进行底层的CAS原子操作,如果:
valueOffset地址的值跟expect的值相等,则说明未被更新,则返回true。否则在死循环中重试直到成功。
继续分析上边的例子,thread1在进行cas前的这段时间内,也许会发生一些事情:thread2将进行了两次CAS操作,先+1,再-1.
thread1在cas时发现内存值还是1,于是进行+1操作,内存值变为2,但是其实thread1后来读取到的1的现场已经发生了变化。
CAS开销:
CAS特点:速度快。
原因:1CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快。
2且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了
CAS的开销在于Cache miss:也就是对某一个变量执行 CAS 操作的 CPU 并不是最后一个操作该变量的那个CPU,而是系统中的其他CPU(假如8核CPU),所以对应的缓存线并不存在于当前对变量执行CAS操作的那个 CPU 的高速缓存中。所以需要将当前变量读取到当前操作CPU的寄存器(高速缓存中)。
首先介绍CPU的体系结构:
上图可以看到一个8核CPU计算机系统,每个CPU有cache(CPU内部的高速缓存,寄存器),管芯内还带有一个互联模块(Interconnect),使管芯内的两个核可以互相通信。在图中央的系统互联模块(SystemInterconnect)可以让四个管芯相互通信,并且将管芯与主存(Memory)连接起来。
数据以“缓存线”为单位在系统中传输,“缓存线”对应于内存中一个 2 的幂大小的字节块,大小通常为 32 到 256 字节之间。
当 CPU 从内存中读取一个变量到它的寄存器中时,必须首先将包含了该变量的缓存线读取到 CPU 高速缓存。
同样地,CPU 将寄存器中的一个值存储到内存时,不仅必须将包含了该值的缓存线读到 CPU 高速缓存,还必须确保没有其他 CPU 拥有该缓存线的拷贝。
比如,如果 CPU0 在对一个变量执行“比较并交换”(CAS)操作,而该变量所在的缓存线在 CPU7 的高速缓存中,就会发生以下经过简化的事件序列:
-
CPU0 检查本地高速缓存,没有找到缓存线。
-
请求被转发到 CPU0 和 CPU1 的互联模块,检查 CPU1 的本地高速缓存,没有找到缓存线。
-
请求被转发到系统互联模块,检查其他三个管芯,得知缓存线被 CPU6和 CPU7 所在的管芯持有。
-
请求被转发到 CPU6 和 CPU7 的互联模块,检查这两个 CPU 的高速缓存,在 CPU7 的高速缓存中找到缓存线。
-
CPU7 将缓存线发送给所属的互联模块,并且刷新自己高速缓存中的缓存线。
-
CPU6 和 CPU7 的互联模块将缓存线发送给系统互联模块。
-
系统互联模块将缓存线发送给 CPU0 和 CPU1 的互联模块。
-
CPU0 和 CPU1 的互联模块将缓存线发送给 CPU0 的高速缓存。
-
CPU0 现在可以对高速缓存中的变量执行 CAS 操作了
所以,就存在这种缓存线迁移的情况,造成CAS开销。