浅谈CAS机制

先来看个实例:启动两个线程,每个线程中让变量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机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值