【多线程与高并发】CAS机制、volatile、synchronize

JUC——CAS机制及AtomicInteger源码分析

CAS简介

CAS即Compare And Swap对比交换,区别于悲观锁,借助CAS可以实现区别于synchronized独占锁的一种乐观锁,被广泛应用在各大编程语言之中。Java JUC底层大量使用了CAS,可以说java.util.concurrent完全是建立在CAS之上的。但是CAS也有相应的缺点,诸如ABAcpu使用率高等问题无法避免。

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。

    (线程1:A = 10,B = 11

    线程2:把变量值更新为11)

  4. 线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。

    (线程1:A = 10,B = 11 A != V的值(10 != 11)提交失败

    线程2 :把变量值更新为11)

  5. 线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋

  6. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。

    (线程1:A = 11,B = 12 A == V的值(11 == 11)

  7. 线程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. 获取当前值

  2. 当前值+1,计算出目标值

  3. 进行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问题。

volatile

synchronize

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值