目录
什么是 CAS
CAS最主要的用途,实现原子类
基于CAS实现自旋锁
CAS的一个典型缺陷,ABA问题
解决 ABA 问题的方法
什么是 CAS
CAS是cpu的一条指令,原子
操作系统就会把这个指令进行封装,提供一些api,就可以在c++zhong被调用
jvm又是基于c++实现的,jvm也能够使用c++调用这样的CAS操作
CAS最主要的用途,实现原子类
boolean,int,long这些类型进行了封装
count++线程不安全
就需要加锁来解决问题
但是认为加锁效率比较低
于是通过cas来实现count++
确保性能同时保证线程安全
使用原子类的目的,就是为了避免加锁
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) {
// 原子地增加计数器
int newValue = counter.incrementAndGet();
System.out.println("New Value: " + newValue);
// 原子地减少计数器
int oldValue = counter.getAndDecrement();
System.out.println("Old Value: " + oldValue);
// 使用 compareAndSet
boolean success = counter.compareAndSet(0, 10);
System.out.println("CompareAndSet Result: " + success + ", Current Value: " + counter.get());
}
}
基于CAS实现自旋锁
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就⾃旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
加锁操作就需要判定锁是否被人占用
如果未被人占用就把当前线程的引用设置到owner中
如果已经被人占用就等待
这里就开始自旋
发现锁已经被占用cas不会执行交换返回false
进入循环,再进行下一次判定
由于循环是空着的,整个循环速度非常快(忙等)
但是一旦其他线程释放了锁,此时该线程就能第一时间拿到这里的锁
CAS的一个典型缺陷,ABA问题
使用cas能够进行线程安全的编程的核心就是先比较相等,内存和寄存器是否相等
这里本质上在进行判定是否有其他线程插进来做了一些修改
认为如果发现这里寄存器和内存值一致,就可以认为是没有线程穿插
过来修改,因此接下来的修改操作就是线程安全的
本来判定内存的值是否是A发现果然是A,说明没有其他线程修改过
但实际上,可能存在一种情况,另一个线程把内存从A修改成B又从B修改回A
ABA 问题示例
假设有一个共享变量 x
,初始值为 A
。线程 1 希望将 x
从 A
更新为 C
,但在执行 CAS 操作之前,线程 2 将 x
从 A
更新为 B
,然后又将 x
从 B
更新回 A
。此时,线程 1 执行 CAS 操作,发现 x
的值仍然是 A
,于是成功地将 x
更新为 C
。尽管 x
的值在中间发生了变化,但 CAS 操作无法检测到这种变化。
解决 ABA 问题的方法
如果换成其他的指标,约定,只能加,不能减,有效避免aba问题
例如,引入另一个概念“版本号”
整数,每次修改一次,版本号就+1
int oldVersion = version;
if(CAS(version,oldVersion,oldVersion+1){
balance -=50;
}
Java 提供了两种方法来解决 ABA 问题:
-
AtomicStampedReference:使用一个时间戳(或版本号)来标记引用,每次更新时同时更新时间戳。这样,即使值从 A 变为 B 再变回 A,时间戳也会发生变化,从而避免 ABA 问题。
-
AtomicMarkableReference:使用一个布尔值来标记引用,类似于时间戳,但更简单。适用于只需要简单标记的情况