CAS 原理

1、什么是 CAS?

CAS:Compare and Swap,即比较再交换。

jdk5 增加了并发包 java.util.concurrent.*, 其下面的类使用 CAS 算法实现了区别于 synchronouse 同步锁的一种乐观锁。JDK 5 之前 Java 语言是靠 synchronized 关键字保证同步的,这是一种独占锁,也是是悲观锁。

**2、CAS 算法理解 **

对 CAS 的理解,CAS 是一种无锁算法,CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。

CAS 比较与交换的伪代码可以表示为:

do {

备份旧数据;

基于旧数据构造新数据;

} while (!CAS ( 内存地址,备份的旧数据,新数据))

img

注:t1,t2 线程是同时更新同一变量 56 的值

因为 t1 和 t2 线程都同时去访问同一变量 56,所以他们会把主内存的值完全拷贝一份到自己的工作内存空间,所以 t1 和 t2 线程的预期值都为 56。

假设 t1 在与 t2 线程竞争中线程 t1 能去更新变量的值,而其他线程都失败。(失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试)。t1 线程去更新变量值改为 57,然后写到内存中。此时对于 t2 来说,内存值变为了 57,与预期值 56 不一致,就操作失败了(想改的值不再是原来的值)。

(上图通俗的解释是:CPU 去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。)

就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的 commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。

**3、CAS 开销 **

前面说过了,CAS(比较并交换)是 CPU 指令级的操作,只有一步原子操作,所以非常快。而且 CAS 避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在 CPU 内部就搞定了。但 CAS 就没有开销了吗?不!有 cache miss 的情况。这个问题比较复杂,首先需要了解 CPU 的硬件体系结构:

img

上图可以看到一个 8 核 CPU 计算机系统,每个 CPU 有 cache(CPU 内部的高速缓存,寄存器),管芯内还带有一个互联模块,使管芯内的两个核可以互相通信。在图中央的系统互联模块可以让四个管芯相互通信,并且将管芯与主存连接起来。数据以 “缓存线” 为单位在系统中传输,“缓存线” 对应于内存中一个 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 操作了

以上是刷新不同 CPU 缓存的开销。最好情况下的 CAS 操作消耗大概 40 纳秒,超过 60 个时钟周期。这里的 “最好情况” 是指对某一个变量执行 CAS 操作的 CPU 正好是最后一个操作该变量的 CPU,所以对应的缓存线已经在 CPU 的高速缓存中了,类似地,最好情况下的锁操作(一个 “round trip 对” 包括获取锁和随后的释放锁)消耗超过 60 纳秒,超过 100 个时钟周期。这里的 “最好情况” 意味着用于表示锁的数据结构已经在获取和释放锁的 CPU 所属的高速缓存中了。锁操作比 CAS 操作更加耗时,是因深入理解并行编程

为锁操作的数据结构中需要两个原子操作。缓存未命中消耗大概 140 纳秒,超过 200 个时钟周期。需要在存储新值时查询变量的旧值的 CAS 操作,消耗大概 300 纳秒,超过 500 个时钟周期。想想这个,在执行一次 CAS 操作的时间里,CPU 可以执行 500 条普通指令。这表明了细粒度锁的局限性。

以下是 cache miss cas 和 lock 的性能对比:

img

**4、CAS 算法在 JDK 中的应用 **

在原子类变量中,如 java.util.concurrent.atomic 中的 AtomicXXX,都使用了这些底层的 JVM 支持为数字类型的引用类型提供一种高效的 CAS 操作,而在 java.util.concurrent 中的大多数类在实现时都直接或间接的使用了这些原子变量类。

Java 1.7 中 AtomicInteger.incrementAndGet () 的实现源码为:

img

img

由此可见,AtomicInteger.incrementAndGet 的实现用了乐观锁技术,调用了类 sun.misc.Unsafe 库里面的 CAS 算法,用 CPU 指令来实现无锁自增。所以,AtomicInteger.incrementAndGet 的自增比用 synchronized 的锁效率倍增。

发布了45 篇原创文章 · 获赞 103 · 访问量 169万+

猜你喜欢

转载自blog.csdn.net/WTUDAN/article/details/105557010
今日推荐