zoukankan      html  css  js  c++  java
  • java中的CAS

    前言

    CAS,Compare and Swap即比较并替换,设计并发算法时常用到的一种技术,Doug lea大神在java同步器中大量使用了CAS技术,鬼斧神工的实现了多线程执行的安全性。

    目前的处理器基本都支持CAS,只不过不同的厂家的实现不一样罢了。CAS有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false

    实现分析

    先看一段代码

    public int a = 1;
    public boolean compareAndSwapInt(int b) {
        if (a == 1) {
            a = b;
            return true;
        }
        return false;
    }

    试想这段代码在多线程并发下,会发生什么?我们不妨来分析一下:

    1. 线程A执行到 a==1,正准备执行a = b时,线程B也正在运行a = b,并在线程A之前把a修改为2;最后线程A又把a修改成了3。结果就是两个线程同时修改了变量a,显然这种结果是无法符合预期的,无法确定a的值。
    2. 解决方法也很简单,在compareAndSwapInt方法加锁同步,变成一个原子操作,同一时刻只有一个线程才能修改变量a。

    CAS中的比较和替换是一组原子操作,不会被外部打断,先根据paramLong/paramLong1获取到内存当中当前的内存值V,在将内存值V和原值A作比较,要是相等就修改为要修改的值B,属于硬件级别的操作,效率比加锁操作高。

    java.util.concurrent.atomic包下的原子操作类都是基于CAS实现的,接下去我们通过AtomicInteger来看看是如何通过CAS实现原子操作的:

    public class AtomicInteger extends Number implements java.io.Serializable { 

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
      try {
        valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value"));
      } catch (Exception ex) {
        throw new Error(ex);
      }
    }

    private volatile int value;

      public final int get() {
        return value;
      }
    }

    1. Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的**原子操作**。
    2. valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的。
    3. value是用volatile修饰的,保证了多线程之间看到的value值是同一份。 接下去,我们看看AtomicInteger是如何实现并发下的累加操作:

    ```
    //jdk1.8实现
    public final int getAndAdd(int delta) {
      return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    public final int getAndAddInt(Object var1, long var2, int var4) {
      int var5;
      do {
        var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }

    ``` 在jdk1.8中,比较和替换操作放在unsafe类中实现。 假设现在线程A和线程B同时执行getAndAdd操作:
    1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据Java内存模型,线程A和线程B各自持有一份value的副本,值为3。
    2. 线程A通过getIntVolatile(var1, var2)方法获取到value值3,线程切换,线程A挂起。
    3. 线程B通过getIntVolatile(var1, var2)方法获取到value值3,并利用compareAndSwapInt方法比较内存值也为3,比较成功,修改内存值为2,线程切换,线程B挂起。
    4. 线程A恢复,利用compareAndSwapInt方法比较,发现手里的值3和内存值2不一致,**此时value正在被另外一个线程修改,线程A不能修改value值**。
    5. 线程的compareAndSwapInt实现,循环判断,重新获取value值,因为value是volatile变量,所以线程对它的修改,线程A总是能够看到。线程A继续利用compareAndSwapInt进行比较并替换,直到compareAndSwapInt修改成功返回true。 整个过程中,利用CAS保证了对于value的修改的线程安全性。

    *** 我们继续深入看看Unsafe类中的compareAndSwapInt方法。
    ```
    public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
    ``` 可以看到,这是一个本地方法调用,这个本地方法在openjdk中依次调用c++代码:unsafe.cpp,atomic.cpp,atomic_window_x86.inline.hpp。下面是对应于intel X86处理器的源代码片段。
    ```

    inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
        int mp = os::isMP();
    //判断是否是多处理器
        _asm {
          mov edx, dest
          mov ecx, exchange_value
          mov eax, compare_value
          LOCK_IF_MP(mp)
          cmpxchg dword ptr [edx], ecx
          }
      }

    ``` 从上面的源码中可以看出,会根据当前处理器类型来决定是否为cmpxchg指令添加lock前缀。
    1. 如果是多处理器,为cmpxchg指令添加lock前缀。
    2. 反之,就省略lock前缀。(单处理器会不需要lock前缀提供的内存屏障效果)

    intel手册对lock前缀的说明如下:
    1. 确保对内存读改写操作的原子执行。 在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性。缓存锁定将大大降低lock前缀指令的执行开销。
    2. 禁止该指令,与前面和后面的读写指令重排序。
    3. 把写缓冲区的所有数据刷新到内存中。 上面的第2点和第3点所具有的内存屏障效果,保证了CAS同时具有volatile读和volatile写的内存语义。



    ###CAS缺点 CAS存在一个很明显的问题,即ABA问题。 如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?
    如果在这段期间它的值曾经被改成了B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。
    针对这种情况,java并发包中提供了一个带有标记的原子引用类"AtomicStampedReference",它可以通过控制变量值的版本来保证CAS的正确性。



    原文链接:http://www.jianshu.com/p/fb6e91b013cc

  • 相关阅读:
    一条insert语句批量插入多条记录
    分析器错误消息: 未能加载类型“WebApplication._Default”
    Avi视频生成缩略图时,提示“尝试读取或写入受保护的内存。这通常指示其他内存已损坏”
    DataGridView 的单元格的边框、 网格线样式的设定【转】
    2015届求职经历(转)
    现在有m组n个有序数组,例如{1,2,3,4},{2,3,4,6},{1,3,5,7},在这些数组中选择第k小的数据,然后返回这个值
    给定数组A,大小为n,现给定数X,判断A中是否存在两数之和等于X
    ASP.Net中使用Report Service
    为自己尝试写点东西吧,程序员们!(转)
    ubuntu菜单面板丢了怎么找回
  • 原文地址:https://www.cnblogs.com/Starshot/p/7605352.html
Copyright © 2011-2022 走看看