15_01_CAS操作

CAS 操作

一、锁的劣势

  • 采用独占的方式访问锁的守护变量
  • 获取锁,存在挂起和恢复线程的问题,这些操作都比较耗时
  • 等待锁时,线程不能做其他的事情
  • 容易出现活跃性故障,比如死锁、活锁等
  • 锁竞争激烈时,线程调度开销会非常大

二、CAS 操作

2.1 基本原理

基本原理:

  1. CAS 包含3个操作数:内存原值 V,期望旧值 A,写入新值 B
  2. 当且仅当 V == A 时,才能将 V 更新为 B

简单地说,就是当 V == A 时,CAS 就认为没有其他线程修改过内存值 V,可以直接写入新值。

  • 当多线程竞争同一个 CAS 时,只有其中一个线程可以成功,其他线程均失败

用代码模拟 CAS 操作大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SimulatedCAS {

private int value;

public synchronized int get() {
return value;
}

public synchronized int compareAndSwap(int expectValue, int newValue) {
int oldValue = value;
if (oldValue == expectValue) {
value = newValue;
}
return oldValue;
}

public synchronized boolean compareAndSet(int expectValue, int newValue) {
return (expectValue == compareAndSwap(expectValue, newValue));
}

}

实际上 CAS 操作会由底层的 JVM 或操作系统实现,是一种原子操作。

CAS 的使用模式通常是这样的:

  1. 首先从内存 V 中读取到值 A,此时 V == A
  2. 然后根据 A 计算得到新值 B
  3. 接着通过 CAS 以原子方式将 V 的值更新为 B
  4. 如果 CAS 成功,则返回;否则一般都需要重新执行 CAS

伪代码如下:

1
2
3
4
5
6
7
while(true) {
A = V;
B = A + 1;
if (CAS(V, A, B)) {
return;
}
}

使用 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 资源
作者

jiaduo

发布于

2022-05-15

更新于

2023-04-03

许可协议