15_01_CAS操作
CAS 操作
一、锁的劣势
- 采用独占的方式访问锁的守护变量
- 获取锁,存在挂起和恢复线程的问题,这些操作都比较耗时
- 等待锁时,线程不能做其他的事情
- 容易出现活跃性故障,比如死锁、活锁等
- 锁竞争激烈时,线程调度开销会非常大
二、CAS 操作
2.1 基本原理
基本原理:
- CAS 包含3个操作数:内存原值 V,期望旧值 A,写入新值 B
- 当且仅当 V == A 时,才能将 V 更新为 B
简单地说,就是当 V == A 时,CAS 就认为没有其他线程修改过内存值 V,可以直接写入新值。
- 当多线程竞争同一个 CAS 时,只有其中一个线程可以成功,其他线程均失败
用代码模拟 CAS 操作大概如下:
1 | public class SimulatedCAS { |
实际上 CAS 操作会由底层的 JVM 或操作系统实现,是一种原子操作。
CAS 的使用模式通常是这样的:
- 首先从内存 V 中读取到值 A,此时 V == A
- 然后根据 A 计算得到新值 B
- 接着通过 CAS 以原子方式将 V 的值更新为 B
- 如果 CAS 成功,则返回;否则一般都需要重新执行 CAS
伪代码如下:
使用 CAS 时,一般都是循环执行代码,直到 CAS 成功为止。
2.2 硬件支持
在多处理器架构中,一般都提供有一些特殊指令,用于管理对共享数据的并发访问:
- Test-and-Set(测试并设置)
- Fetch-and-Increment(获取并递增)
- Compare-and-Swap(比较并交换)
- Load-Linked(加载链接)
- Store-Conditional(存储条件)
这些特殊指令,提供了某种形式的“原子“读写操作,可以安全地访问共享数据。
2.3 JVM 支持
硬件指令虽然提供了 CAS 的实现,但是在 Java 代码中是不能直接使用硬件指令的。
所以 JVM 会将 CAS 代码编译成底层硬件提供的最有效方法:
- 在支持 CAS 的平台上,JVM 会将 CAS 代码编译为相应的(多条)机器指令
- 在不支持 CAS 的平台上,JVM 将会使用自旋锁来代替 CAS 操作
JVM 为 Java 代码执行 CAS 操作提供了底层的支持。
2.4 锁和 CAS 的区别
- 独占锁是一项悲观技术;而 CAS 是一项乐观的技术
- 获取锁失败时,线程会阻塞挂起;而竞争 CAS 失败时,线程不会阻塞,而是直接返回
- 锁的获取可能导致操作系统级别的锁定、线程挂起以及上下文切换等;而 CAS 操作不会
- 锁可以自动处理竞争问题(JVM 实现);而 CAS 需要由调用者处理竞争问题(循环 CAS 直到成功)
- 锁定时,至少需要一次 CAS,所以锁的开销基本都会比 CAS 大
- 竞争激烈时,锁的性能会更好;而竞争不激烈时,CAS 的性能会更好
- 竞争激烈时,CAS 会导致很多线程空自旋等待,浪费 CPU 资源