Java中的CAS详解

Posted 贺兰猪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的CAS详解相关的知识,希望对你有一定的参考价值。

背景

在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁
锁机制存在以下问题:

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。

(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile是不错的机制,能解决内存可见性和指令重排序问题,但不能保证原子性。因此对于同步最终还是要回到锁机制上来。

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。

什么是 CAS?

CAS(Compare-And-Swap)是比较并交换的意思,它是一条 CPU 并发原语,用于判断内存中某个值是否为预期值,如果是则更改为新的值,这个过程是原子的。

 CAS机制当中使用了3个值:内存地址V,旧的预期值A,计算后要修改的新值B

  •  两个线程同时对内存值V进行操作,V初始值为1
  • 线程1、线程2都对V加1计算,预期值A=1,新值B=2
  • 线程2先提交,预期值A==V,更新成功,将V更新为2
  • 线程1提交时4,发现预期值A=1,V=2,A!=V,提交失败,重新获取内存值V=2
  • 线程1自旋,V=2,A=2,B=3,重新比较A==V成立,然后更新V=3,最后V=3结束

更新一个变量的时候,只有当变量的预期值 A 和内存地址 V 中的实际值相同时,才会将内存地址 V 对应的值修改为 B,这个操作就是CAS

CAS 基本原理

CAS 主要包括两个操作:Compare和Swap。

CAS 是一条 CPU 的原子指令,执行必须是连续的,执行过程中不允许被中断。

CAS底层用到的Unsafe类,Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

下面是AtomicInteger类的getAndSet方法

public final int getAndSet(int var1) 
        return unsafe.getAndSetInt(this, valueOffset, var1);
    


var1 Atomiclnteger对象本身。
var2该对象值得引用地址。
var4需要变动的数量。
var5是用过var1 var2找出的主内存中真实的值。
用该对象当前的值与var5比较:
如果相同,更新var5+var4并且返回true,
如果不同,继续取值然后再比较,直到更新完成。

 public final int getAndSetInt(Object var1, long var2, int var4) 
        int var5;
        do 
            var5 = this.getIntVolatile(var1, var2);
         while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    

 
    /**
     * Atomically update Java variable to <tt>x</tt> if it is currently
     * holding <tt>expected</tt>.
     * @return <tt>true</tt> if successful
     */
    public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);

 多线程如何保证数据的原子性?

应用举例:假设线程A和线程B两个线程同时执行getAndAddlInt操作(分别跑在不同CPU上) :

  • AtomicInteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
  • 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  • 线程B也通过getlntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  • 这时线程A恢复,执行compareAndSwaplnt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  • 线程A重新获取value值,因为变量value被volatle修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功。

CAS存在的问题

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

ABA问题。

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了,CAS操作无法感知。

解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号+1,那么A-B-A 就会变成1A - 2B-3A。

自旋开销。

CAS冲突后会重复尝试,如果资源竞争非常激烈,自旋长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

应用端的方案就是限制自旋次数,避免过度消耗CPU。

只能保证单个变量的原子性。

当对一个共享变量执行操作时,可以使用 CAS 来保证原子性,如果要对多个共享变量进行操作时,CAS 是无法保证原子性的,比如需要将 i 和 j 同时加 1:

i++;j++;

优化:

1、使用 synchronized 进行加锁;

2、将多个变量操作合成一个变量操作。AtomicReference 类来保证引用对象之间的原子性,把多个变量放在一个对象里来进行CAS操作

AtomicReference 关键方法:

/**
     * Atomically sets the value to the given updated value
     * if the current value @code == the expected value.
     * @param expect the expected value
     * @param update the new value
     * @return @code true if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(V expect, V update) 
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    

详解java中CAS机制所导致的问题以及解决——内存顺序冲突

CAS机制

指的是CompareAndSwap或CompareAndSet,是一个原子操作,实现此机制的原子类记录着当前值的在内存中存储的偏移地址,将内存中的真实值V与旧的预期值A做比较,如果不一致则说明内存中的值被其他线程修改过了,返回false,否则将新值B存入内存。

Java内部是使用本地调用类unsafe实现的。

Java原子类底层原理就是采用CAS机制。

可能会出现什么问题

  1. aba问题:

线程1取出A之后被阻塞了,此时线程2把内存中A改为B,一系列操作后又改为A,此时线程1恢复执行,取内存中的A与手中的A做比较,发现没有变,继续执行。

然而此时俩A虽然可能是一样的,但是其实是被修改过的。例如,线程1需要替换的是一个栈顶,从A替换成B,但是执行前线程2抢占到时间片,对栈做出了一系列出栈操作,然后又将A入栈,此时线程1恢复,发现栈顶还是A,所以替换成B,但此时这个栈已经不是原来的栈了。

解决思路——版本号:

在比较的时候加入版本号的比较,每次修改时也修改版本号。

1.5开始的AtomicStampedReference就是采用了的版本号比较。

  1. 执行开销大:

CAS如果长时间不成功会一直自旋循环,会产生不少的执行开销。并且为了自旋结束时避免内存顺序冲突,CPU会对流水线进行重排,这样会严重影响cpu性能。

解决思路——pause指令:

pause指令能让自旋失败时cpu睡眠一小段时间再继续自旋,从而使得读操作的频率低很多,为解决内存顺序冲突而导致的流水线重排的代价也会小很多。 

内存顺序冲突——当自旋锁快要释放的时候,持锁线程会有一个store命令,外面自旋的线程会发出各自的load命令,而此处并没任何 happen-before 排序,所以处理器是乱序执行,所以为了避免load出现在store之前此时会进行流水线清空再重排序,会严重影响cpu效率,Pause指令的作用就是减少并行load的数量,从而减少重排序时所耗时间。(不懂load和store可以去看看JMM(java内存模型)的资料)

  1. 只能保证一个共享变量的原子性操作:

解决思路——1.5开始的AtomicReference可以保证引用的原子性,可以把多变量放入对象中进行原子操作。

 

从自己word笔记中复制过来的,没图,而且格式很多有问题,有时间再补。

以上是关于Java中的CAS详解的主要内容,如果未能解决你的问题,请参考以下文章

Java中CAS详解

详解java中CAS机制所导致的问题以及解决——内存顺序冲突

Java CAS 比较并且更换值

java并发之CAS详解

高频面试java高级进阶之锁?与CAS详解#yyds干货盘点#

并发编程的灵魂:CAS机制详解