深刻理解 CAS 原理 | Java

CAS(乐观锁)

CAS 全称为 Compare And Swap 翻译过来就是比较而且交换java

  • Synchornized 是悲观锁,线程一旦获得锁,其余的线程就只能挂起了
  • cas 的操做则是乐观锁,他认为本身必定会拿到锁,因此他会一直尝试,直到成功拿到为止;

CAS 机制

在看到 Compare 和 Swap 后,咱们就应该知道,CAS 里面至少包含了两个动做,分别是比较和交换,在如今的 CPU 中,为这两个动做专门提供了一个指令,就是CAH 指令,由 CPU 来保证这两个操做必定是原子的,也就是说比较和交换这两个操做只能是要么所有完成,要么所有没有完成markdown

CAS 机制中使用了三个操做数,内存地址,旧的预期值,要修改的值;多线程

例如 a + 1 的操做,a 默认=0,app

1,在多个线程修改一个值 a 的时候,会将 a copy 一份到本身的线程内存空间中(预期值),此时预期值就是 a ,要修改的值就是 a+1 的结果,结果就是 1(要修改的值),因为是多个线程,因此每一个线程内部都会获得 a = 1。函数

2,接着就会执行比较而且交换, 让线程中的预期值和主内存中的 a 进行比较,若是相同,就会提交上去,若是不相同,说明 a 的值已经被别的线程修改过了,因此就会提交失败(这个比较和提交的操做是原子性的)。提交失败以后,线程就会从新获取 a 的值,而后重复这一操做。这种重复操做的方式称为自旋ui

栗子:this

前提:线程 A,B,主内存中的变量 count = 0;atom

  1. 线程A: 要修改 count 值,因此 copy 一份到本身的内存中,而后执行了 + 1 的操做,此时线程A中 count 预期值是 0,要修改的值为 1spa

  2. 线程B :也修改 count 值,也执行了 + 1 的操做,此时线程 B 中 count 的预期值是 0,要修改的值为 1,线程

  3. 线程B :开始提交到主内存了,提交的时候判断预期值 和 主内存的 count 是同样的,因此就会提交成功,这时主内存 count =1

  4. 线程A :也开始提交了,可是在判断的时候发现预期值是 0,但主内存是1,不相等,因此,提交失败,而后就会放弃本次提交。

  5. 线程A :提交失败以后,就与从新执行步骤 1 的操做。

这种方式最终能够理解成一种 无阻塞的多线程争抢资源的模型。

问题

  • ABA

    仍是上面的栗子,

    在线程 A 执行 + 1,操做的时候,线程 B 已经将 + 1的结果提交的主内存了,可是这个时候他又执行了 - 1的操做提交到主内存,而且这个过程快于线程 A。

    这个时候线程 A 进行判断和交换,发现修改的值和主内存的值相同,而后将计算的结果提交了。


    在线程 A 执行的过程当中,线程 B 修改了值,而且将值又修改了回去,虽说结果并无变化,可是值已经被操做过了

    这就是典型的 ABA 问题

    那么如何解决呢?


    其实解决起来很是简单,只须要增长一个版本戳便可,在更新值的时候判断一下版本戳便可。

    在 Java 中也有使用版本戳的实现,就是 AtomicMarkableReferenceAtomicStampedReference

    AtomicMarkReference :只关心这个变量有没有被动过

    AtomicStampedReferrence :不但关心这个变量有么有动过,而且关心这个变量被动了几回,例如

    val asr = AtomicStampedReference<String>("345", 0)
    
    fun main() {
        val oldStamp = asr.stamp
        val oldReference = asr.reference
        println("$oldReference ---- $oldStamp")
    
        val t1 = Thread {
            println("${Thread.currentThread().name} 当前变量值:${asr.reference} 当前版本:${asr.stamp}" +
                    " ${asr.compareAndSet(asr.reference, "3456", asr.stamp, asr.stamp + 1)}")
        }
        val t2 = Thread {
            println("${Thread.currentThread().name} 当前变量值:${asr.reference} 当前版本:${asr.stamp}" +
                    " ${asr.compareAndSet(asr.reference, "34567", asr.stamp, asr.stamp + 1)}")
        }
        t1.start()
        t1.join()
        t2.start()
        t2.join()
    
        println("${asr.reference} ---- ${asr.stamp}")
    }
    345 ----  0
    Thread-0   当前变量值:345  当前版本:0   true
    Thread-1   当前变量值:3456  当前版本:1   true
    34567 ----  2
    复制代码

    在修改字符串的时候,要传入已经修改过的字符串和版本号,负责就会修改错误

  • 开销问题

    在 CAS 期间,线程是不会休息的,线程若是长时间没法提交,内部就一直在进行自旋,这样就会产生比较大的内存开销

  • CAS 只可以保证一个共享变量的原子操做

    CAS 只能保证对一个内存地址进行原子操做,因此说使用范围会有必定限制

    例如:若是在执行 a+1 的下面加上,b+1,c +1,这种状况就会出现问题,这种时候反而使用 Syn 比较方便


    其实 Java 中也提供了能够修改多个变量的原子操做

    AtomicReference:将须要修改的包装成一个对象,而后使用 AtomicReference 的 compareAndSet 方法进行替换便可

    fun main() {
        val user = User("张三", 31)
        val atomicReference = AtomicReference<User>(user)
        println("${atomicReference.get().name} --- ${atomicReference.get().age}")
    
        atomicReference.compareAndSet(user, User("李四", 20))
        println("${atomicReference.get().name} --- ${atomicReference.get().age}")
    }
    
    class User(val name: String, val age: Int)
    复制代码

    上面的代码就保证了修改多个变量,实际上就是更新对象

实例

在 java 的 atomic 包下,一系列以 Atomic 开头的包装类,如 AtomicBoolean AtomicInteger 等,分别用于 int,bool,long 等类型的原子操做,其原理都是用的 cas

核心实现以下:

//使用将给定函数应用于当前值和给定值的结果,以原子方式更新当前值,并返回更新后的值。 该函数应无反作用,由于当尝试更新因为线程间争用而失败时,可能会从新应用该函数。 应用该函数时,将当前值做为其第一个参数,并将给定的update做为第二个参数 
public final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) {
    //预期值和要更新的值
        int prev, next;
        do {
            //获取到预期值,也就是当前值
            prev = get();
            //计算要更新的值
            next = accumulatorFunction.applyAsInt(prev, x);
            //更新成功则退出循环,不然从新计算
        } while (!compareAndSet(prev, next));
        return next;
}

 //注意:这个比较而且 set 的操做是原子性的
 //参数:指望–指望值 更新–新价值
 //返回值:
 //若是成功,则为true 。 错误返回表示实际值不等于指望值
public final boolean compareAndSet(int expect, int update) {
     return U.compareAndSwapInt(this, VALUE, expect, update);
}
复制代码

若是本文有帮助到你的地方,不胜荣幸,若有文章中有错误和疑问,欢迎你们提出!

相关文章
相关标签/搜索