一、CAS是什么
全称Compare-And-Swap,即“比较并交换”,相当于一个原子的操作,同时完成“读取内存,比较是否相等,修改内存”这三个步骤,本质上需要cpu指令的支持
判断内存某个位置的值是否为预期值。若是则更新为新值,实现比较与交换;若不是则无事发生
CAS伪代码:
boolean CAS(address, expectValue, swapValue) {
//如果内存位置的值与预期原值相同,就把原值用新值替换,并返回true
if (&address == expectedValue) {
&address = swapValue;
return true;
}
//否则无事发生,返回false
return false;
}
可以使用CAS完成一些操作,进一步替代“加锁”,这也给编写线程安全的代码引入了新思路
基于CAS实现线程安全的方式,也称为“无锁编程”
优点:保证线程安全,同时避免阻塞(效率)
缺点:代码会更复杂,不好理解;只能够适合一些特定场景,不如加锁更普适
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号
二、CAS实现原子类
在标准库中提供的 java.util.concurrent.atomic 包里面有原子类,原子类内部用的是CAS,所以性能要比加锁实现i++高很多,原子类有以下几个
- AtomicBoolean
- AtomicInteger
- AtomicInntegerArry
- AtomicLong
- AtomicReference
- AtomicStampedReference
来看一下经典案例:
public class demo12 {
public static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count="+count);
}
}
利用AtomicInteger类对int进行包装,此时进行自加(基于CAS指令实现)就是原子的了
import java.util.concurrent.atomic.AtomicInteger;
public class demo12 {
public static AtomicInteger count=new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
count.getAndIncrement();//count++
/*
count.getAndDecrement(); count--
count.incrementAndGet(); ++count
count.decrementAndGet(); --count
count.getAndDecrement(2); count-=2
*/
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 5000; i++) {
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count.get());
}
}
来看下这里的自加操作是如何实现的
其中又调用了unsafe类中的getAndAddInt()方法
第362行调用unsafe类内的cas方法,jvm会实现cas汇编指令
若再继续点进compareAndSwapInt()方法,就会发现接下来是native本地方法,也就是jvm源码中,使用C++实现的逻辑
可以说,CAS本质上是cpu提供的指令,被操作系统封装提供成api,再被jvm封装也提供成api供程序员使用
以上不太好理解,我们直接使用伪代码来理解下AtomicInteger类如何进行自加操作
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
来多线程穿插执行下看下是如何利用CAS实现原子类的:
上述过程没有加锁,但是实现了线程安全
前面我们遇到的“线程不安全”本质上是进行自增操作的过程中,线程穿插执行了
CAS这里也是让CAS不要穿插执行,核心思路与加锁相似
不过,加锁是通过阻塞的方式避免穿插执行;CAS则是通过重试的方式避免穿插执行
这是非常妙的方法,但是这是需要一定的硬件支持的!
三、CAS实现自旋锁
伪代码如下:
public class SpinLock {
private Thread owner = null;//记录当前锁被哪个线程获取到了,若为null即未加锁状态
public void lock(){
//cas(此锁拥有者,null,调用lock方法的线程的引用)
/*
通过 CAS 看当前锁是否被某个线程持有.
如果这个锁已经被别的线程持有, 那么就自旋等待.
如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
*/
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
//释放锁,即把锁拥有设为null
public void unlock (){
this.owner = null;
}
}
四、CAS的ABA问题
CAS进行操作的关键,是通过重试 值“没有发生变化”来作为“没有其他 线程穿插执行”的判定依据
就是当判定相等时,不确定value是一直没有更改,还是被别的线程更改却又被另外线程改回来了
比如买手机,买到的是一个翻新机,这就是又把二手的又翻新回来出售,我个人是无法确定的
看如下场景,通过重试value值有无发生变化来判断有无穿插执行,这里完全没问题,发现被穿插执行,最终只扣款一次
但若在t2扣款完成后而t1又没有进行CAS时,t3又转入一笔钱500
(把value又改回1000再执行t1的CAS(1000,1000,500))
这样t1就又可以成功进行一次扣款,也就是说这仨线程一台戏一共扣了1000!
这就实现了把值A->B->A,最后导致bug!
只要让判定的数值,按照一个 方向增长即可(不要反复横跳)
可以引入版本号version,约定每次修改余额version++,此时在使用CAS判定时,就不是直接 判定数值了,而是判定版本号,看版本号是否变化:若版本号不变,则表示没有其他线程穿插执行
总的来说,解决ABA问题就是:
给要修改的数据引入版本号,在CAS比较数据当前值和旧值的同时,也要比较版本号是否符合预期。若发现当前版本号和之前读到的版本号一致,就真正 执行修改操作,并让版本号自增;若发现当前版本号比 之前读到的版本号大,就认为操作失败
在实际开发中,并不会直接使用CAS,用的都是封装好的