Java-并发-CAS

19 篇文章 0 订阅

Java-并发-CAS

0x01 摘要

本文主要讲讲AQS(AbstractQueuedSynchronizer)中大量使用的CAS,以及著名的ABA问题。

0x02 CAS基本概念

乐观锁在Java中的一个重要实现就是CAS,全称为 Compare and Swap,就是在内存级别比较和原子性地替换值。

在Java里,是用的sun.misc.Unsafe类来实现了很多相关的native修饰的CAS方法。最常用的应该就是compareAndSwapInt方法。

可以看看ReentrantLock里的lock()方法:

public void lock() {
    // 使用同步锁进行锁定
    sync.lock();
}

接着看看默认的非公平同步锁的lock方法的实现:

final void lock() {
	 // 这一步其实就是期望的值是0,如果确实是0就把它更新为1
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

这里就是调用的AQS里的compareAndSetState方法

// 用来表示同步锁状态的变量 
private volatile int state;
// 上述状态变量在AbstractQueuedSynchronizer类中的域偏移值
private static final long stateOffset;
stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));

// 在当前state变量值等于excpect时,就原子性的设置state变量为给定的update值
protected final boolean compareAndSetState(int expect, int update) {
    // 尝试用CAS的方法将当前AQS实例state变量设为1
    // 如果成功就返回true
    // 返回false意味着该变量的当前值不为expect
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

0x03 ABA问题

3.1 什么是ABA问题

CAS看起来很方便,好像啥都不用我们操心,但有可能会导致著名的ABA问题。

我们仔细考虑,CAS操作其实分为两步:

  1. 获取某时刻该变量在offset的值
  2. 比较并替换该值

也就是说,1和2不是一个原子性操作,那么就有可能发生获取的时候是期望值,但在比较和替换时其实该值已经被其他线程修改了的情况,导致有一次CAS操作的结果被覆盖。这就是ABA问题。

ABA问题
具体来说,这个ABA过程如下:

  1. 线程1做CAS(X, A, C),获取到该变量的X值为A
  2. 线程2做CAS(X, A, B),获取到该变量X的值为A符合预期
  3. 线程2将X值设为B
  4. 线程2做CAS(X, B, A),获取到该变量X的值为B符合预期
  5. 线程2又将X值设为A
  6. 线程1发现X值为A符合CAS传入的预期值,于是将X的值设为C
  7. 结果就是线程1和2都认为此次CAS操作成功,但其实里面有个中间变化线程2根本就不知道,这并不符合我们的预期。可能会导致意外的严重后果。

对于使用CAS操作的原子类,例如AtomicInteger,均存在ABA问题,所以我们通常会对变量增加版本号来解决问题,JDK中的AtomicStampedReference也帮我们实现了这个功能,不过这里需要注意存在一个坑。

3.2 ABA问题示例

private static AtomicInteger atomicInt = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
    Thread intT1 = new Thread(new Runnable() {
        @Override
        public void run() {
            atomicInt.compareAndSet(100, 101);
            atomicInt.compareAndSet(101, 100);
        }
    });
    
    Thread intT2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean c3 = atomicInt.compareAndSet(100, 101);
             //true
            System.out.println(c3);       
        }
    });
    
    intT1.start();
    intT2.start();
}

最后会发现线程二输出的cas结果为true,也就是说他没有感知到值变量atomicInt被被修改后又被重置为100

3.3 AtomicStampedReference原理

3.3.1 重要的内部类和构造方法
// 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);
    }
}
private volatile Pair<V> pair;
/**
 * 使用指定的初值创建一个新的AtomicStampedReference
 *
 * @param initialRef 我们关心的对象的初值
 * @param initialStamp 版本戳的初值
 */
public AtomicStampedReference(V initialRef, int initialStamp) {
    pair = Pair.of(initialRef, initialStamp);
}
3.3.2 compareAndSet
/**
 * 使用期望的目标对象和版本戳来设值
 * 1.比较当前目标引用和目标对象引用地址
 * 2.然后比较当前版本戳和参数中的期望版本戳是否相同
 * 3.比较当前目标引用和新的目标对象引用是否相同且当前版本戳和新的目标版本戳相同,说明已经被其他线程修改
 * 4.或是成功以CAS方式修改了<当前目标对象, 当前版本戳>为新的<新目标对象, 新版本戳>
 * 5. 3和4满足任意条件就返回true
 *
 * @param expectedReference the expected value of the reference
 * @param newReference the new value for the reference
 * @param expectedStamp the expected value of the stamp
 * @param newStamp the new value for the stamp
 * @return {@code true} if successful
 */
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)));
}
3.3.3 casPair
// 这个方法就是用UNSAFE类来直接CAS方式替换Pair对象
private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
3.3.4 小结

其实AtomicStampedReference就是在普通CAS基础上加上了个版本戳,和对象形成了Pair,可以避免ABA问题。

3.4 AtomicStampedReference解决ABA问题示例

private static AtomicStampedReference<Integer> atomicStampedRef = 
	new AtomicStampedReference<Integer>(100, 0);

public static void main(String[] args) throws InterruptedException {
Thread refT1 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedRef.compareAndSet(100, 101, 
                    atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
            atomicStampedRef.compareAndSet(101, 100, 
                    atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
        }
    });
    
    Thread refT2 = new Thread(new Runnable() {
        @Override
        public void run() {
            int stamp = atomicStampedRef.getStamp();
            System.out.println("before sleep : stamp = " + stamp);    // stamp = 0
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
            boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
            //false
            System.out.println(c3);        
        }
    });
    
    refT1.start();
    refT2.start();
}

最终会发现线程2输出的CAS结果为false,因为与atomicStampedRef关联的stamp因为线程1的操作导致已经发生了变化。

0x04 原理

透过前面的代码,可以看到compareAndSwapInt是一个JNI调用。

jdk8/hotspot/src/share/vm/prims/unsafe.cpp中可以找到以下内容:

{CC"compareAndSwapInt",  CC"("OBJ"J""I""I"")Z",      FN_PTR(Unsafe_CompareAndSwapInt)},

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);
  // 获取该filed内存地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // 调用Atomic.cmpxchg方法
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

这个原子性是现代处理器新增的硬件指令支持的,在IA64、x86指令群中有cmpxchg指令完成的CAS功能。

具体是使用LOCK CMPXCHAG指令实现。

CMPXCHAG指令本身不是原子性的,他用于比较并交换操作数,CPU对CAS的原语支持。

所以需要加上LOCK,由CPU保证被其修饰的指令的原子性,实现原理(详情参见:多处理器下的数据一致性):

  • 依赖内存有序模型,来保证读取指令有序;

  • 通过总线锁或缓存一致性,保证被修饰指令操作的数据一致性:

    • 当访问的数据在系统内存时,通过在总线使用锁实现原子性(保证只有一个CPU能使用);
    • 当访问的数据在处理器的缓存时,通过缓存一致性协议实现原子性;

    常见的缓存一致性协议有:MESI,MESIF(MESIF是缓存行的状态标识,M:Modified, E: Exclusive, S:Shared, I:Invalid, F: Forwad),通过标记缓存行的状态和处理器间的通讯来实现。

举个LOCK栗子

  • Java的DCL中若返回的变量不加volatile修饰,则可能会由于指令重排导致另一个线程获取到一个非完全初始化的对象。

    而当volatile修饰的变量所在的代码段成为热点,被JIT编译为汇编代码后,会增加LOCK前缀来禁止指令重拍和数据一致;

LOCK CMPXCHAG保证原子性的不同CPU个数场景:

  • 单核
    无需加LOCK前缀,即使增加也会被替换为nop
  • 多核
    需要加LOCK前缀

鉴于本人能力有限,就不再继续向下了。有兴趣的读者可以研究下jdk8/hotspot/src/share/vm/runtime/atomic.cpp

也可参考文章:

0x05 应用

AtomicInteger的原子自增方法incrementAndGet底层就用了CAS方法,代码如下:

public final int incrementAndGet() {
	// 这里在getAndAddInt就完成了自增并返回了原始值,这里加1就是得到的新值作为结果返回
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

Unsafe.getAndAddInt如下

// 获取对象var1的偏移量为var2的值,并加上var4,直到成功
// 返回的是原来的值
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
    	// 获取该值
        var5 = this.getIntVolatile(var1, var2);
        // 如果该值为var5,且成功替换为新值var5+var4就返回,否则一直循环该过程
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

0x06 总结

以上就是对CAS和ABA问题的分析,可以看到java中的解决方法就是将对象和另一个对象关联组成pair,然后通过UNSAFE直接CAS替换该pair对象。与上面提到的AtomicStampedReference类似的还有AtomicMarkableReference,只是实现的原理有很小的不同而已。

CAS存在的问题

  • ABA
  • 不适合写多的场景
    竞争大,重试很多,持续自旋,CPU开销大
  • 只能保证一个共享变量的原子操作
    多个变量操作时无法保证原子性。可以使用AtomicReference来保证引用对象之间的原子性,所以可把多个变量放在一个对象里来进行CAS操作。

0xFF 参考文档

JAVA中CAS-ABA的问题解决方案AtomicStampedReference

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值