JUC——CAS机制及AtomicInteger源码分析
CAS简介
CAS即Compare And Swap
对比交换,区别于悲观锁,借助CAS可以实现区别于synchronized独占锁的一种乐观锁,被广泛应用在各大编程语言之中。Java JUC底层大量使用了CAS,可以说java.util.concurrent
完全是建立在CAS之上的。但是CAS也有相应的缺点,诸如ABA
、cpu使用率高
等问题无法避免。
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
举个例子:
在内存地址V当中,存储着值为10的变量。
此时线程1想把变量的值增加1. 对线程1来说,旧的预期值A=10,要修改的新值B=11.
在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
(线程1:A = 10,B = 11
线程2:把变量值更新为11)
线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。
(线程1:A = 10,B = 11 A != V的值(10 != 11)提交失败
线程2 :把变量值更新为11)
线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。
(线程1:A = 11,B = 12 A == V的值(11 == 11)
线程1进行交换,把地址V的值替换为B,也就是12.
(线程1:A = 11,B = 12 A == V的值(11 == 11)地址V的值更新为12
从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。
在java中除了上面提到的Atomic系列类,以及Lock系列类夺得底层实现,甚至在JAVA1.6以上版本,synchronized转变为重量级锁之前,也会采用CAS机制。
CAS的缺点:
1) CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
CAS本质上是通过自旋来判断是否更新的,那么问题来了,如果多次旧预期值不等于内存值的情况,那么这个自旋将会自旋下去,而自旋过久将会导致CPU利用率变高。
2) 不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
只是单纯对单个共享对象进行CAS操作,保证了其更新获取的原子性,无法对多个共享变量同时进行原子操作。这是CAS的局限所在,但JDK提供同时了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
3) ABA问题
这是CAS机制最大的问题所在。
CAS的自旋,循环体中做了三件事:
-
获取当前值
-
当前值+1,计算出目标值
-
进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤
这里需要注意的重点是get方法,这个方法的作用是获取变量的当前值。
如何保证获取的当前值是内存中的最新值?很简单,用volatile关键字来保证(保证线程间的可见性)。
compareAndSet方法的实现:
// 1、获取UnSafe实例对象,用于对内存进行相关操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 2、内存偏移量
private static final long valueOffset;
static {
try {
// 3、初始化地址偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 4、具体值,使用volatile保证可见性
private volatile int value;
compareAndSet方法的实现很简单,只有一行代码。这里涉及到两个重要的对象,一个是unsafe,一个是valueOffset。
什么是unsafe呢?
AtomicInteger
中依赖于一个叫Unsafe
的实例对象,我们都知道,java语言屏蔽了像C++那样直接操作内存的操作,程序员不需手动管理内存,但话说回来,java还是开放了一个叫Unsafe
的类直接对内存进行操作,
至于valueOffset对象,是通过unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存地址。
对于给定的某个字段都会有相同的偏移量,同一类中的两个不同字段永远不会有相同的偏移量。也就是说,只要对象不死,这个偏移量就永远不会变,可以想象,CAS所依赖的第一个参数(内存地址值)正是通过这个地址偏移量进行获取的。
正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。
举个例子:
ABA问题,其他线程修改过N值,并在当前线程修改时已经把它改回N值
参考:CAS的ABA问题
ABA问题解决方式:
解决方案是通过版本号,Java提供了AtomicStampedReference
来解决。AtomicStampedReference
通过包装[E,Integer]
的元组来对对象标记版本戳stamp
,从而避免ABA
问题。
总结
1. java语言CAS底层如何实现?
利用unsafe提供的原子性操作方法。
2.什么事ABA问题?怎么解决?
当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。
利用版本号比较可以有效解决ABA问题。