先来看个实例:启动两个线程,每个线程中让变量countNum循环累加100次。
public class TestCas {
private static int countNum = 0;
// private volatile static AtomicInteger countNum = new AtomicInteger();
public static void main(String[] args) {
for(int i=0;i<2;i++) {
new Thread(new Runnable() {
public void run() {
for(int j=0;j<100;j++) {
countNum++;
// synchronized (TestCas.class) {
// countNum.incrementAndGet();
// }
}
}
}).start();;
}
/**
* 要想看到结果必须先让主线程让出CPU执行权,不然System.out.println(countNum)会先输出,看到的结果为0
*/
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(countNum);
}
}
多次运行结果都是小于200.因为自增操作是不安全的。首先我们可能想到的是给自增操作加入同步锁。
synchronized (TestCas.class) {
countNum++;
//countNum.incrementAndGet();
}
加了同步锁之后,最终的结果countNum一定是200但是在某些情况下,可能这种方案在性能上不是很好,因为:
synchronized关键字会让没有得到锁资源的线程进入BLOCKED(阻塞)状态,而后在争夺锁资源后回复为RUNNABLE(可运行)状态,这个过程中设计到操作系统用户模式和内核模式的切换,代价比较高。虽然在java1.8位synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过渡,但是在最终转变为重量级锁之后,性能仍然比较低。那有什么替换方案吗?
有,使用Java中的原子操作类。
Java中的原子操作类都在java.util.concurrent.atomic包下面,以Atomic开头的包装类,如:AtomicInteger,AtomicBoolean,AtomicLong,它们分别作用于Integer,Boolean,Long类型的原子性操作。
使用AtomicInteger改造上面程序:
private volatile static AtomicInteger countNum = new AtomicInteger();
for(int j=0;j<100;j++) {
countNum.incrementAndGet();
}
运行的结果为是200。
那Java这些原子性操作类的原理是什么呢?其实就是我们要将的CAS机制!
什么是CAS?
cas的全称是Compare and Swapd,即:比较并替换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新这个位置的值。
底层原理是:基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些原子操作类便是使用了这些封后的接口。
简单说:CAS机制当中使用了3个基本操作数,内存地址V,旧的预期值A,要修改的新值B。更改一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
举个例子加深理解:
1.内存地址V存储着值为10的变量。
2.此时线程1想要把变量的值增加1.对线程1来说,旧的预期值A=10,要更改的新值B=11.
3.在线程1要提交之前,另一个线程2抢先一步,把内存地址V中的变量率先更新为11.
4.此后线程1开始提交更新时,首先进行A和地址V的实际值比较(Compare),发现A不等V的实际值,提交失败。
5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12,这个重新尝试的过程被称为自旋。
6.这次比较幸运,没有被抢先执行,线程1进行比较(compare),发现A和地址V的实际是是相等的。
7.线程1进行替换(swap),b把地址V的值替换为B,也就是12.
从思想进行分类,synchronized属于悲观锁,悲观锁认为程序中的并发情况严重,必须一个一个来。CAS属于乐观锁,认为程序中的并发情况不严重,所以让线程不断去尝试更新。
其中从上面分析,CAS还是有缺点的,如:
1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力
2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3.ABA问题
举个例子分析:
小明有100元存款,要用这个提款机来提款50元。
由于提款机硬件出了点小问题,小明的提款操作被同时提交了两次,开启了两个线程,两个线程都是获取当前值100元(旧的预期值A),要更新成50(要替换的新值B)元。
理想情况下,应该一个线程更新成功,另一个线程更新失败,小明的存款只被扣一次。
首先线程1执行成功,把余额从100改成50.线程2因为某种原因阻塞了,这时候小明的妈妈刚好给小明汇款50元(线程3)。线程2仍然是阻塞状态,线程3执行成功,余额从50改成100。
线程2 恢复运行,由于阻塞之前已经获取了“旧的预期值”为100,并且经过Compare检测与现在地址V的实际值100相等,所以成功把变量值100更新成50了,余额变成50了。人生最痛苦的事情就是:人活着,钱没了,钱没了,钱没了。
这就是ABA问题。
那有什么办法可以解决呢?
有,在Compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。
举个例子说明一下:
假设地址V中存储着变量值A,当前版本是01。线程1获得了当前值A和版本号01,想要更新为B,但是被阻塞了。
这时候,内存地址V中的变量发生了多次改变,版本号提升为03,但是变量值仍是A。
随后线程1恢复运行,进行比较Compare操作,经过比较,线程1所获得的值和地址V中的实际值都是A,但是版本号不相等,所以更新失败。
Java中,AtomicStampedReference类就实现了用版本号做比较的CAS机制。