zoukankan      html  css  js  c++  java
  • 多线程原理篇(5)-CAS与原子操作

    10.1 乐观锁与悲观锁的概念

       锁可以从不同的角度分类。其中,乐观锁和悲观锁是一种分类方式。

       悲观锁:

      悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

      乐观锁:

      乐观锁又称为“无锁”,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为CAS的技术来保证线程执行的安全性。

      由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说乐观锁天生免疫死锁。

      乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。

    10.2 CAS的概念

    CAS的全称是:比较并交换(Compare And Swap)。在CAS中,有这样三个值:

    • V:要更新的变量(var),更新变量的主内存的最新值
    • E:预期值(expected),更新操作时候,线程缓存中的值
    • N:新值(new), 更新操作要赋予的值

    比较并交换的过程如下:

    判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。

    所以这里的预期值E本质上指的是“旧值”。

    我们以一个简单的例子来解释这个过程:

    1. 如果有一个多个线程共享的变量i原本等于5,我现在在线程A中,想把它设置为新的值6;
    2. 我们使用CAS来做这个事情;
    3. 首先我们用i去与5对比,发现它等于5,说明没有被其它线程改过,那我就把它设置为新的值6,此次CAS成功,i的值被设置成了6;
    4. 如果不等于5,说明i被其它线程改过了(比如现在i的值为2),那么我就什么也不做,此次CAS失败,i的值仍然为2。

    在这个例子中,i就是V,5就是E,6就是N。

    那有没有可能我在判断了i为5之后,正准备更新它的新值的时候,被其它线程更改了i的值呢?

    不会的。因为CAS是一种原子操作,它是一种系统原语,是一条CPU的原子指令,从CPU层面保证它的原子性

    当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作

    用比较通俗的话:我需要对一个值进行修改,我不会直接修改,而是将当前的值和要修改的值传入,如果此时内存中的确为当前的值,那么就进行修改,否则修改失败。 他的思想是一种乐观锁的思想。

    10.3 Java实现CAS的原理 - Unsafe类

    前面提到,CAS是一种原子操作。那么Java是怎样来使用CAS的呢?我们知道,在Java中,如果一个方法是native的,那Java就不负责具体实现它,而是交给底层的JVM使用c或者c++去实现。

    Unsafe中对CAS的实现是C++写的,它的具体实现和操作系统、CPU都有关系。

    Linux的X86下主要是通过cmpxchgl这个指令在CPU级完成CAS操作的,但在多处理器情况下必须使用lock指令加锁来完成。当然不同的操作系统和处理器的实现会有所不同,大家可以自行了解。

    当然,Unsafe类里面还有其它方法用于不同的用途。比如支持线程挂起和恢复的parkunpark, LockSupport类底层就是调用了这两个方法。还有支持反射操作的allocateInstance()方法。

    10.4 原子操作-AtomicInteger类源码简析

     

    从名字就可以看得出来这些类大概的用途:

    • 原子更新基本类型
    • 原子更新数组
    • 原子更新引用
    • 原子更新字段(属性)

    这里我们以AtomicInteger类的getAndAdd(int delta)方法为例,来看看Java是如何实现原子操作的。

    public class AtomicInteger extends Number implements java.io.Serializable {
        private static final long serialVersionUID = 6214790243416807050L;
    
        // setup to use Unsafe.compareAndSwapInt for updates
        private static final Unsafe unsafe = Unsafe.getUnsafe();//unsafe
        private static final long valueOffset;//用于获取value相对Java对象的“起始地址”的偏移量。
        static {
            try {
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
        private volatile int value;//volatile修饰的基础类型
       //..  
      
      /**
      * Atomically adds the given value to the current value.
      *
      * @param delta the value to add
      * @return the previous value
      */
      public final int getAndAdd(int delta) {
      return unsafe.getAndAddInt(this, valueOffset, delta);//调用unsafe类的方法,继续跟踪
      }
    //..
    }

      

       public final int getAndAddInt(Object var1, long var2, int var4) {
            int var5;
            do {
                var5 = this.getIntVolatile(var1, var2);//读操作,原子性,获取旧值(相对于执行CAS的时间点)
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//CAS操作,原子性由CPU层面保证,(与内存中的值做比较,失败则循环,成功则返回旧值)
    
            return var5;
        }
    

      

    继续看源码。前面我们讲到,CAS是“无锁”的基础,它允许更新失败。所以经常会与while循环搭配,在失败后不断去重试。

    这里声明了一个v,也就是要返回的值。从getAndAddInt来看,它返回的应该是原来的值,而新的值的v + delta

    这里使用的是do-while循环。这种循环不多见,它的目的是保证循环体内的语句至少会被执行一遍。这样才能保证return 的值v是我们期望的值。

    循环体的条件是一个CAS方法,可以看到,最终其实是调用的我们之前说到了CAS native方法

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    

      再回到循环条件上来,可以看到它是在不断尝试去用CAS更新。如果更新失败,就继续重试。那为什么要把获取“旧值”v的操作放到循环体内呢?其实这也很好理解。前面我们说了,CAS如果旧值V不等于预期值E,它就会更新失败。说明旧的值发生了变化。那我们当然需要返回的是被其他线程改变之后的旧值了,因此放在了do循环体内。

    10.5 CAS实现原子操作的三大问题

    这里介绍一下CAS实现原子操作的三大问题及其解决方案。

    10.5.1 ABA问题

    所谓ABA问题,就是一个值原来是A,变成了B,又变回了A。这个时候使用CAS是检查不出变化的,但实际上却被更新了两次。

    ABA问题的解决思路是在变量前面追加上版本号或者时间戳。从JDK 1.5开始,JDK的atomic包里提供了一个类AtomicStampedReference类来解决ABA问题。

    这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果二者都相等,才使用CAS设置为新的值和标志。

    10.5.2 循环时间长开销大

    CAS多与自旋结合。如果自旋CAS长时间不成功,会占用大量的CPU资源。

    解决思路是让JVM支持处理器提供的pause指令。

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

    10.5.3 只能保证一个共享变量的原子操作

    这个问题你可能已经知道怎么解决了。有两种解决方案:

    1. 使用JDK 1.5开始就提供的AtomicReference类保证对象之间的原子性,把多个变量放到一个对象里面进行CAS操作;
    2. 使用锁。锁内的临界区代码可以保证只有当前线程能操作。

    总结:锁分乐观锁和悲观锁。CAS是compare and swap,比较旧值和最新值是否相等,是的话,则对旧值进行操作比如加减。CAS是CPU层面上保证的原子性操作。java.util.concurrent.amomic类使用volatile来修饰值,结合CAS操作,来实现共享变量复合操作的原子性,比锁更轻量级。

    原文转载地址:http://redspider.group:4000/article/02/10.html

  • 相关阅读:
    java生成json字符串的方法
    JSON的三种解析方式
    Android Studio你不知道的调试技巧
    Android 打开URL
    build.gradle中引入jar
    Spark RDD/Core 编程 API入门系列之map、filter、textFile、cache、对Job输出结果进行升和降序、union、groupByKey、join、reduce、lookup(一)
    NovaMind *的安装、和谐破解到永久使用
    小Q书桌的下载、安装和使用
    作业提交过程分析(源码)
    SparkContext的初始化过程分析(源码)
  • 原文地址:https://www.cnblogs.com/knsbyoo/p/14031776.html
Copyright © 2011-2022 走看看