Java的CAS乐观锁原理解析

CAS全称 Compare And Swap(比较与交换),在不使用锁的状况下实现多线程之间的变量同步。属于硬件同步原语,处理器提供了基本内存操做的原子性保证。juc包中的原子类就是经过CAS来实现了乐观锁。java

CAS算法涉及到三个操做数:算法

  • 须要读写的内存值 V。
  • 进行比较的旧值A (指望操做前的值)
  • 要写入的新值 B。

当且仅当 V 的值等于 A 时,CAS经过原子方式用新值B来更新V的值(“比较+更新”总体是一个原子操做),不然不会执行任何操做。
通常状况下,“更新”是一个不断重试的过程。多线程

JAVA中的sun.misc.Unsafe类,提供了并发

  • compareAndSwapInt
  • compareAndSwapLong

等方法实现CAS。函数

  • 示例

J.U.C包内的原子操做封装类


看一下AtomicInteger的源码定义:高并发

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();
    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;

各属性的做用:性能

  • unsafe: 获取并操做内存的数据
  • valueOffset: 存储value在AtomicInteger中的偏移
  • value: 存储AtomicInteger的int值,该属性须要借助volatile关键字保证其在线程间的可见性
    ​​

接着查看自增方法incrementAndGet的源码时,发现自增函数底层调用的是unsafe.getAndAddInt
可是因为JDK自己只有Unsafe.class,只经过class文件中的参数名,并不能很好的了解方法的做用,因此咱们经过OpenJDK 8 来查看Unsafe的源码:this

// ------------------------- JDK 8 -------------------------
// AtomicInteger 的自增方法
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// Unsafe.class
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;
}

// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
   int v;
   do {
       v = getIntVolatile(o, offset);
   } while (!compareAndSwapInt(o, offset, v, v + delta));
   return v;
}

由源码可看出,getAndAddInt()循环获取给定对象o中的偏移量处的值v,而后判断内存值是否等于v。线程

  • 若是相等则将内存值设置为 v + delta
  • 不然返回false,继续循环进行重试,直到设置成功才能退出循环,而且将旧值返回

整个“比较+更新”操做封装在compareAndSwapInt()中,经过JNI使用CPU指令完成的,属于原子操做,能够保证多个线程都可以看到同一个变量的修改值。3d

JDK经过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。若是相等,就把要写入的新值 B 存入内存中。若是不相等,就将内存值 V 赋值给寄存器中的值 A。而后经过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。

CAS的问题

循环+CAS

自旋的实现让全部线程都处于高频运行,争抢CPU执行时间的状态。CAS操做若是长时间不成功,会致使其一直自旋,若是操做长时间不成功,会带来很大的CPU资源消耗。

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

对一个共享变量执行操做时,CAS可以保证原子操做,可是对多个共享变量操做时,CAS是没法保证操做的原子性的。
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,能够把多个变量放在一个对象里来进行CAS操做。

ABA问题(没法体现数据的变更)

CAS须要在操做值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。可是若是内存值原来是A,后来变成了B,而后又变成了A,那么CAS进行检查时会发现值没有发生变化,可是其实是有变化的。
ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操做封装在compareAndSet()中。
compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,若是都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
不过目前来讲这个类比较”鸡肋”,大部分状况下 ABA 问题并不会影响程序并发的正确性,若是须要解决 ABA 问题,使用传统的互斥同步可能比原子类更加高效。

只能保证一个共享变量的原子操做。对一个共享变量执行操做时,CAS可以保证原子操做,可是对多个共享变量操做时,CAS是没法保证操做的原子性的。
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,能够把多个变量放在一个对象里来进行CAS操做。

Java 8 更新

固然这都是由 Doug Lea 大佬亲自为 Java 操刀

更新器

DoubleAccumulator

LongAccumulator

计数器

DoubleAdder

LongAdder


计数器加强版,高并发下性能更好
频繁更新但不太频繁读取的汇总统计信息时使用分红多个操做单元,不一样线程更新不一样的单元

只有须要汇总的时候才计算全部单元的操做


T1执行后,A 变成了B

T3又开始执行了, B变成了A

T2开始执行, A变成了C

  • 问题点:
  • 经历的A -> B -> A过程,可是对于线程2,没法感知数据发生了变化
相关文章
相关标签/搜索