java并发编程之-CAS详解

乐观锁,悲观锁

悲观锁

每次获取数据都很悲观,觉得会有人同时修改,所以每次在拿数据的时候都会上锁。也就是共享资源每次都只给一个线程使用,其他的线程会阻塞着,当第一个线程执行完后再让出资源给其他线程,synchronized和ReentranLock 采用的都是悲观锁的思想

乐观锁

每次获取数据的时间都很乐观,认为不会被人修改。因此每次操作都不会上锁。只是在更新的时候去判断在这期间内有没有人去更新过这个数据,如果有就重新获取,并在此进行判断,一直循环执行,直到拿到没有修改过的数据。CAS就是乐观锁的一种实现方式。

CAS

CAS的全程是compare and swap 比较并交换,是一种原子操作,同事CAS是一种乐观的机制,在JUC包中很多功能都是基于CAS实现的,比如ReenterLock中的AQS,AtomicInteger等其底层都是基于CAS来实现的原子操作

CAS的原理

CAS实现的原理很简单,三个参数,一个当前内存值V,旧的预期值A,即将更新的值B,当预期值与内存值相同时,将内存值修改为B并返回True,否则什么都不做,并返回false。

以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); }
  }
.....
}

在静态方法中用到了Unsafe类,这个类可以提供硬件级别的原子操作,在日常开发中很少用到,底层是基于C/C++实现的,所以类中的方法大都是被native修饰的。

AtomicInteger类存储的值是在value字段中,并且获取了Unsafe的实例,在静态代码块中,还获取了value字段在内存中的偏移量valueOffset。

以AtomicInteger类中的getAndIncrement()方法来举例,这个方法中就是通过CAS来保障了并发安全

public final int getAndIncrement() {
  return unsafe.getAndAddInt(this, valueOffset, 1);
}

进入到getAdnAddInt方法中

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;
}

var5 通过 this.getIntVolatile(var1, var2)方法获取,这是个 native 方法,目的是获取 var1 在 var2 偏移量的值,其中 var1 就是 AtomicInteger, var2 是 valueOffset 值。

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

接下来compareAndSwapInt 就是实现了CAS的核心方法,如果war1中的值和var5相等,就证明没有其他线程改变过这个变量,就把这个var1中的value值更新为var5+var4,其中这个var4是更新时的增量值,;反之如果没有更新那么CAS就通过自选的方式继续尝试更新,这一步也是原子操作。

CAS是通过C++内联汇编实现的。Java代码需要通过JNI才能调用,在unsafe.cpp中。

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

以上代码大致意思是:先拿到value在内存中的地址。在通过Atomic::cmpxchg实现比较替换,其中参数x是要更新的值,e是原内存的值。

CAS的常见问题

ABA问题

问题概述:加入线程1取出A值,这时线程2同时也取出了A值,此时的线程1处于挂起状态,线程2将A值修改为B值,然后又修改成了A,这时候线程1继续执行,发现A值还是没变,这时线程1同样会执行成功。

通常在乐观锁的实现中都会带一个version字段来记录更新的版本,来避免并发操作带来的问题,Java中AtomicStampedReference类实现了这个处理方式。

AtomicStampedReference 的内部类 Pair

private static class Pair<T> {
  final T reference;
  final int stamp;
  private Pair(T reference, int stamp) {
    this.reference = reference;
    this.stamp = stamp;
  }
  static <T> Pair<T> of(T reference, int stamp) {
    return new Pair<T>(reference, stamp);
  }
}

每个 Pair 维护一个值,其中reference维护对象的引用,stamp维护修改的版本号。

AtomicStampedReference中的compareAndSet:

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
  Pair<V> current = pair;
  return
    expectedReference == current.reference &&
    expectedStamp == current.stamp &&
    ((newReference == current.reference &&
      newStamp == current.stamp) ||
     casPair(current, Pair.of(newReference, newStamp)));
}

在compareAndSet中,如果要修改内存中的值,不但要值相同,还要版本号相同

自旋问题

自旋就是操作失败后继续循环操作,这种方式也成为自旋锁,是一种乐观锁的机制,一般来说都会给一个限定的自旋次数,防止进入死循环。

自旋锁的有点就是不需要休眠当前线程,因为自旋锁使用者一般保持锁的时间非常短,因此选择自旋而不是休眠当前线程时提高并发性能关键点,减少了很多不必要的上下文切换开销。

但是如果CAS一直不成功,会造成长时间的自旋,给CPU带来很大的开销。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值