一、CAS的介绍
在JDK1.之前,java的多线程都是靠synchronized来保证同步的,这会引发很多性能问题,例如死锁。但随着Java的不断完善,JNI使得java能越过JVM直接调用本地方法,例如CAS。CAS ,它用于实现多线程同步的原子指令,允许算法执行读-修改-写的操作,而无须担心其他线程同时修改变量。
二.CAS的工作原理
CAS全称Cpmpare and swap,能够通过比较内存和寄存器的值是否相等,看是否要交换内存和另一个寄存器的值。
上述比较,交换都是通过一条CPU指令来完成的,所以也就可以认为它是原子的。线程安全那一篇我们谈过,造成线程安全问题的第二点就是进行的操作不是原子的,我们通过加锁和给变量加volatile去解决这个问题的。而CAS本身已经是原子的,基于这点,那么他又给我们打开了另一扇大门,无锁编程。
三.CAS的运用
实现原子类
前面我们聊过多个线程同时访问同一个变量并且进行修改会触发线程安全问题,如果多个线程同时对某一个变量进行++操作那么必然造成线程安全问题。因为对某一个变量进行++,其中涉及,load,add,save,由于线程的抢占式的执行和不是原子性的操作导致结果出现偏差(在前几篇细讲过)。因此java标准库中提供了一组原子类。
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class java19 {
public static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for(int i=0;i<1000;i++){
atomicInteger.getAndIncrement();
}
});
Thread thread1 = new Thread(()->{
for(int i=0;i<1000;i++){
atomicInteger.getAndIncrement();
}
});
thread.start();
thread1.start();
thread.join();
thread1.join();
System.out.println(atomicInteger);
}
}
接着看分析下面代码
这串代码还是通过比较内存中的值和寄存器1的值是否相等,如果相等交换寄存器2和内存的值,并且返回true,不会进入循环内部,会接着继续检查下一轮。如果内存中的值和寄存器1中的值不相等则会返回false,进入循环内部,把内存中的值给寄存器1。
这里比较相等,其实就是在检查当前的内存值是不是变了,是不是被别的线程穿插进来改了。进一步发现出现其他线程穿插的情况,立即重新读取内存的值,准备进行下一轮。
当2个线程并发的执行++操作的时候,如果不加任何的限制,意味着有时候,这两++是串行的,能计算正确,有的时候这两是穿插进行的,这个时候就会出现问题。
加锁保证线程安全:通过锁,避免出现穿插。
原子类CAS保证安全:借助CAS来识别是否出现穿插的情况,如果没有出现穿插,此时直接修改,如果出现穿插,就重新读取内存的最新值在尝试修改。
二.CAS的ABA问题
CAS的关键要点是比较内存和寄存器中的值,通过比较的值是否相等,来判定内存的值是否发生变化。如果内存的值变了,存在其他线程进行了修改。如果内存的值没变,没有别的线程修改,接下来进行的修改就是安全的。
如果这里的值没变,就一定没有别的线程修改吗?假设银行里面存钱,银行卡里面有100块钱,先取出50块钱,然后在存入50块钱,假设取出50块钱后,另一个人又给我打入50块钱,那么按照CAS的逻辑去判断会出现错误。
当已经扣除50块钱,其他人转入50,把50又改为100,然后用100和寄存器的值判断,显然是错误的,这就是ABA问题。虽然上述操作概率是比较小的,但也需要去考虑,假设上述情况出现的概率是万分之一,并非是一个小数字,站在高并发大数据的角度,其实概率已经很大了。
ABA,CAS问题基本思路是OK的,但是主要是修改操作能进行反复横跳,就容易让CAS失效。CAS判定的是值相同,实际上期望的是值没有变化过,如果约定值只能单向变化(只能增大,减小)。账户余额好像不能只增长,不减小,但是版本号可以,此时痕量余额是否变化,会从余额转换为版本号。使用CAS判定版本号是否相同,如果版本号相同,则数据没有改过。