目录
1.CAS及使用
1.1. CAS概念
1.2.原子类的使用
1.3.CAS使用自旋锁
2.CAS的ABA问题
2.1.问题介绍
2.2.ABA问题解决方式
1.CAS及使用
1.1. CAS概念
(1)CAS,其实是一种操作的简称,全称为:Compare and swap。
(2)通过全称,我们就可以知道,它的作用就是:比较并且交换,这里的操作数不是两个,而是三个。
(3)作用1:类似一个方法,有三个参数,A、V、B
比较A与V比较是否相等;如果相等,将B和A交换;反之,不进行任何操作。
上述的比较:是将A内存中的值和V寄存器中的值进行比较,如果它们里面的值相等,就会将B寄存器中的值赋值给A内存中。也就是将内存A中的值和寄存器B中的值进行交换,就不会考虑交换后寄存器B中的值。
(4)作用2:CAS可以保证这一系列操作是原子性的(保证线程安全)
上述将了一堆概念,下面介绍使用CAS的使用;因为JVM对CAS进行了封装,所以我们可以直接使用CAS提供的api就可以了。
1.2.原子类的使用
原子类也就是对cas封装后的抽象表示,但是cas不推荐大家使用,性价比低,安全隐患大。
(1)原子类
大约有下面这些原子类,通过下面的类操作一些数,是原子性的。
这里的类很多,我们这里只介绍一个:AtomicInteger
(2)AtomicInteger类
这个是我们比较常用的类,是对int的封装,对一个int数据进行++或者--操作,可以保证其是原子性,也就是线程安全。
- 创造一个AtomicInteger对象
private static AtomicInteger count = new AtomicInteger(0);
这句代码的意思就是:定义一个int遍历count,并且赋值为0,相当于int count = 0;只不过这样创造的变量可以调用这个类里面的方法进行++或者--操作,并且保证原子性。
- 五个方法
方法名 | 作用 |
---|---|
getAndIncrement() | 后置++ |
incrementAndGet() | 前置++ |
getAndDecrement() | 后置-- |
decrementAndGet() | 前置-- |
getAndAdd() | +=操作 |
演示:
后置++
前置++;
同理:前置--和后置--也是一样的操作
+=操作;
(3)使用AtomicInteger类进行多线程操作
单单使用AtomicInteger操作体现不出来它的特点,我们一样是使用对比手法。
操作:使用两个线程对count各进行50000次++操作
- 普通int变量
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
System.out.println("++操作前:"+count);
Thread t1 = new Thread(()->{
for(int i=0;i<50000;i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for(int i=0;i<50000;i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("++操作后:"+count);
}
明明都++了50000次,但是结果却不是100000,很明显的线程不安全。
- AtomicInteger变量
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
System.out.println("++操作后:"+count);
Thread t1 = new Thread(()->{
for(int i=0;i<50000;i++) {
count.getAndIncrement();
}
});
Thread t2 = new Thread(()->{
for(int i=0;i<50000;i++) {
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("++操作后:"+count);
}
结果也是非常的安全呀,这就是原子类的特点
1.3.CAS使用自旋锁
这里我们不作深入的介绍,使用一个伪代码进行介绍即可。
既然是基于自旋锁,那么发挥的作用就类似自旋锁一样。什么是自旋锁:就是当一个锁被占用后,另一个线程再去争夺,机会发生阻塞,此时另一个线程一直在循环等待这把锁释放,就是自旋锁。
- 伪代码基本介绍
- CAS部分介绍
这也是就是自旋锁的思想和做法。
2.CAS的ABA问题
cas也是存在问题的,称为ABA问题。
2.1.问题介绍
(1)问题来源
CAS的核心操作是:比较相等,然后进行交换操作。也就是说,只要发现两个对象相等,就会进行交换操作。因此,就会存在一个问题:两个对象相等,但是不代表某个对象没有被修改过,CAS也无法发现问题。也就是其中一个对象的值从A被修改成B,最后又修改回A,然后进行比较操作,也是相等的。
所以ABA问题也就是:CAS无法区分,当前的数据在比较相等前,是否被修改过。
虽然ABA问题产生有危害的Bug几率很小,但是某些情况,就是严重问题。
(2)ABA问题产生的严重BUG例子
举例时间:
场景:PP去ATM前面取钱
假设:PP的余额有1000元,目的要取出500。并且取款的操作是以CAS的方式执行
CAS(A、B、C)
A表示银行卡中应该有的余额数,B表示当前余额,C表示后来的余额
伪代码:
解释一下:比如,我们进入“取款函数”,就会显示余额B=1000元,并且把余额赋值给A(也就是说,当前银行卡里面是有1000元);点击取款500,C=A-500。
此时:如果A==B,说明还未取款成功,就会扣款,也就是A=C,将银行账户里面的钱-500,完成取款500元的操作。此时A!=B,说明已经取款了,就会结束循环,取款操作结束。
上面是我们预期的正常操作,但是如果按照下面的执行,就极有可能发生严重的BUG
极端操作:我们多次点击取款500操作(第一次点击取款未生效,继续第二次),然后这个时候,恰好有另一个人给你转账500元。以上就会产生3个线程
并且按照下面的执行顺序:
这种情况下,也就会产生严重的bug。下面来了解解决方式
2.2.ABA问题解决方式
因为上述的余额是可变的,但是中间是否被修改过,我们也不清楚,所以不能让它来作为CAS的判断依据。
(1)我们引入一个“版本号”作为CAS判断依据,也可以使用其他的变量,规定只能进行单向操作(也就是只能++或者--)
(2)版本号作用:每次操作完成后,版本号都要进行+1操作
(3)示例:
按照这种方式,就可以有效的避免CAS的ABA问题