目录
一.为什么要有CAS
二.什么是CAS机制
三. CAS工作原理
四.CAS的缺点
五.关于CAS中的ABA问题
解决方案
我们在多线程安全中有一个经典的双线程++问题
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
count++;
}
});
t1.join();
t2.join();
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println(count);
}
就是两个线程同时进行++这个操作,就会导致结果出错
这里结果应该=20000
我们由此知道了++操作,分为三步,应该加锁确保他的操作是原子性的
//可以将++操作打包到一个枷锁的方法里面
synchronized public void func(){
count++;
}
但是这样枷锁和解锁,实际上效率并不高效,由此我们引入了一个新的概念
CAS机制
简单来说就是进行原子性操作
比如,我们可以将上面的加锁和解锁替换成CAS中的原子操作
所谓CAS的原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。
让我们用一用
import java.util.concurrent.atomic.AtomicInteger;
public class demo3 {
//将int count变为AtomicInteger count = new AtomicInteger()
public static AtomicInteger count = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
//使用原子性的++操作
count.incrementAndGet();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
//使用原子性的++操作
count.incrementAndGet();
}
});
t1.join();
t2.join();
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println(count);
}
}
结果就好了
我们拿++这个操作来举例
假如我现在有两个线程
线程1和线程2
此时线程1中,有一个数字10开始进行++操作
如果线程2此刻没有进行++操作,也就是说目前只有线程1在进行这个操作的话
就是如下情况
但是如果,此时线程2也有个++操作,并且比线程1先执行完毕的话
所以这样永远能保证++操作的原子性
1) CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
2) 不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3) ABA问题(重点问题)
这是CAS机制最大的问题所在。
经典ABA例子:取钱问题
假如现在有一位老哥在CAS的取款机上取钱
这个时候,这个CAS的取款机,在一瞬间开创了线程A和线程B
相当于两个线程在同时进行-操作
如果是这样的话,按照CAS的机制,那么我们理想情况下,应该是这样
但是如果遇到了一种意外情况
线程B遇到了BUG,停止了运行
恰好这个时候,滑稽老哥的妈妈给他汇过去50元
然后这个时候,BUG修复了,线程B开始运行
于是我们的滑稽老铁就白白损失了50元
这就是ABA问题
其实B修好了bug之后,不应该以原来的100作为基准
而是应该以线程C没有修改之前的线程A运行的结果作为基准
这里的B是拿C运行的结果作为基准来和原来进行比较,这样肯定是错误的
加上版本号
将线程A和B设置为同一版本号,经过比较
如果版本号不同的话,那么我们直接让这个操作失效就好了