Java并发之CAS理解

CAS

CAS即Compare and Swap(比较和交换),是可以保证线程安全的一种较为高效的方法,首先来看一下例子:

int i = 0;
public void increment(){
  i++;  //这里分为三步:1.读取i的值,  2. 对i进行+1操作 3.将+1后的i刷新回主内存
}  

这里如果有2个线程同时来调用这个方法,但是结果不一定是2,因为每个线程都有自己的工作内存,首先假设线程1调用这个方法,首先读取i = 0到自己的工作内存,然后再进行+1操作,此时线程2也去调用这个方法,获取i = 0到自己的工作内存,然后进行+1操作,最后不管是线程1还是2先将结果刷新回主内存,结果都是1.更多线程调用这个方法,也会是相似的问题。

解决方案有两种,一是使用synchronized,如下:

int i = 0;
public synchronized void increment(){
  i++;  //这里分为三步:1.读取i的值,  2. 对i进行+1操作 3.将+1后的i刷新回主内存
} 

由于加了锁,那么同一时间只能够有一个线程去调用这个方法,其他线程会被阻塞,因此,这样能够保证线程安全,当一个线程操作完后,会唤醒其他被阻塞的线程继续进行调用操作。但是由于线程的切换以及锁的获取以及释放是有很大的性能开销的,如果频繁的使用synchronized,那么可能会降低应用的性能。

第二个解决方法就是使用CAS,CAS如何来保证线程安全呢,它的原理是,首先线程1读取了i = 0到自己的工作内存,然后进行 + 1操作,在刷新回主内存之前,先判断一下此时主内存的值是否和 自己获取到的初始i值相等,如果相等,说明没有其他线程改变这个值,那么就可以将自己改变后的值刷新回主内存,线程2要刷新回主内存之前,也首先判断一下主内存的i值是否和自己获取到的初始值相等,如果不等,那么就重新获取一次i值,再进行一次+1操作,最后将结果刷新回主内存,如果再次获取到的i值还是和上一次获取的不同,那么就循环的进行这个过程,这个过程可以模拟为:

public void increment(int i){
  while(compareAndSet(i,k,j){
  		int k = i;
		int j = k + 1;
  }
  return j;

CAS其实也是三步,读取、比较、以及写入到工作内存,看起来不是线程安全的, 但是事实上操作系统会对这个保证线程安全,因为CAS对应的是操作系统平台的汇编指令。这样用CAS而非synchronized,就能够避免用锁造成的性能开销。Java提供了很多CAS的原子类,供我们使用:

在这里插入图片描述

CAS实例

使用AtomicInteger对int值进行自增:

AtomicInteger integer = new AtomicInteger();
 for (int i = 0;i < 10;i++){
    integer.incrementAndGet();
 }
 System.out.println("result:"+integer.get());   //结果就是期望的结果:10

上面的例子就是线程安全的, 不管有多少个线程调用该函数,得到的结果都是正确的,因为内部CAS保证了线程的安全。

CAS存在的ABA问题

从上面可以看到,CAS通过比较以及重新设置来保证线程安全操作,那么可能会存在一种情况,线程A首先对i = 0 进行了+1,然后再进行了-1操作,此时线程B要刷新回主内存之前,先比较自己初始获取到的i=0,和此时主内存的i=0相等,那么认为没有其他线程改变过i值,从而总结将自己的值刷新回主内存,对于int或者boolean这种类型的值这个不是太大问题。但是对于如果使用了上图中的引用原子类AtomicReference的话,就可能会对结果造成影响,因为该引用其实已经变化过了,但是线程B仍然认为它没有变化,而是简单的将主内存的值进行替换。这个就是CAS的ABA问题,那么要如何解决呢?Java也给我们提供了解决方法,就是利用版本控制,每个值加入一个版本的概念,当线程B比较自己的工作内存中的i值和主内存的i值相等,但是版本不相等,就会再次获取主内存的值,重新进行一次自己的内部值更改工作,示例如下:

		 AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(1,1);

        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //线程1先将reference的值由1变到2,版本号 + 1
            reference.compareAndSet(1, 2, reference.getStamp(), reference.getStamp() + 1);
            //再将版本值由2变到1,版本号再 + 1,此时reference和初始值就不一样了,因为版本号已经更改
            reference.compareAndSet(2, 1, reference.getStamp(), reference.getStamp() + 1);
        }).start();

        new Thread(() -> {
            //线程2首先获取初始版本号 &  初始值,然后睡眠一段时间,等待线程1先将结果改变。
            int stamp = reference.getStamp();
            int value = reference.getReference();
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //此时线程2再将结果刷新回主内存,由于此时主内存版本号已经更改了,尽管此时值还是1,但是这个操作还是会失败,由此线程2也可以得知,原来主内存的值经历了ABA
            boolean result = reference.compareAndSet(value, 2, stamp, stamp + 1);
            System.out.println("result:"+result); //false

            //此时需要更新线程2工作内存的value和stamp,即重新获取一次主内存的引用值和版本号
            value = reference.getReference();
            stamp = reference.getStamp();

            result = reference.compareAndSet(value,2,stamp,stamp + 1);
            System.out.println("result:"+result); //此时结果就是true。
        }).start();
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值