并发编程 之 CAS

1、加锁和CAS

如果是多线程并发去给变量 int i=0 去做递增操作,那么肯定会出现并发不安全的情况,例如重复出现相同的数字。

那么可以利用 synchronized锁+volatile 关键字来保证线程并发安全。

  • synchronized 关键字是保证,执行递增操作的方法,同一时间只能有一个线程执行,如果都是带了 synchronized 关键字的方法,那么可以同时保证原子性、可见性和有序性。但如果有其他线程调用了对于这个变量读操作的方法,而往往读操作不需要加synchroinzed关键字,所以就无法保证可见性问题
  • 所以为了同时保证所有方法中,变量的可见性,需要对变量增加 volatile 关键字去修饰。

但是我们都知道,当存在并发高的场景加锁,synchronized将会升级为重量级锁;如果无法成功获取锁的线程,都会进入等待,接着又需要被唤醒竞争。

所以对于像只是对基础变量做操作,其实是属于很快并且很轻的操作,如果能不加锁就能达到线程安全,那么就会更好。

所以,JDK在concurrent.atomic包中,对于基础类型,都提供了对应的原子类,如 AtomicInteger、AtomicLong等等。他们的原理是利用底层的 Unsafe 类的compareAndSwapInt 方法来对变量进行更新,而其中的原理就是无锁化的 CAS 操作,即比较和设置。利用轮询+CAS来避免重量级的加锁操作。

2、AtomicInteger

AtomicInteger 中有三个属性:

 // 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:就是我们上面提到的,JDK提供的原子类,都是利用 Unsafe 提供的 compareAndSwapInt 来做无锁化的 CAS 操作
  • valueOffset:记录变量value在 AtomicInteger 类中的偏移量,Unsafe更新value值时是根据这个偏移量定位的
  • value:记录值,我们可以看到它也被volatile关键字修饰,保证变量的可见性(不同线程、不同方法)

核心方法

AtomicInteger 提供了很多方法,如compareAndSet(int expect, int update)、 getAndIncrement()、getAndDecrement()、getAndAdd(int delta)等等

其中 compareAndSet 方法其实就是调用了 Unsafe#compareAndSet 方法,如果给的预期值和内存中的一致,就更新成功返回true、否则更新失败返回false。

而其他方法基本都是同样的原理,例如拿 getAndIncrement() 方法举例,它就是调用 unsafe.getAndAddInt(this, valueOffset, 1) 方法;而 Unsafe#getAndAddInt 方法里面,就是利用轮询+CAS操作,直到更新成功。

/**
* var1:当前对象
* var2:值对应偏移量,即 AtomicInteger 中的 valueOffset
* var4:要增加的值
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // 获取当前对象 valueOffset 这个偏移量对应的值,即预期值
        var5 = this.getIntVolatile(var1, var2);
        // 调用unsafe的compareAndSwapInt方法,尝试设置新的值
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

3、LongAdder

从上面可以看到,如果我们利用 atomic 包下的原子类来保证并发安全,那么会有一个问题,那就是如果同一时间多个线程进行操作,那么会出现一种情况:多个线程同时在自旋,这样是非常得浪费资源的。

那么,对于 AtomicLong,JDK后续有出了一个增强版的:LongAdder,它支持多个线程同一时间进行并发操作而不会出现线程问题。

LongAdder 继承了 Striped64,Striped64 里面包含了所有关于CAS和支持多线程并发操作的逻辑。其中原理是,Striped64 里面有三个非常重要的属性,两个属性都加了 volatile 关键字保证可见性:

  • cells:如果同一时间超过一个线程操作,那么就会为后续的线程分配一个Cell,线程执行的操作,例如增加或减少,都会在这个Cell里面
  • base:基础值,如果只有一个线程操作,那么都是基于这个值去操作,如果是多线程,那么其他线程会在上面提到的Cell操作,而汇总需要使用 base 和 cells,主要是遍历 cells数组,然后拿到每个 Cell 里面的值,然后累加到 base 上面,最后返回。
  • cellsBusy:主要用于并发安全地为线程创建对应的 Cell,创建 Cell 也是利用 CAS 来完成的。cellsBusy==0并且CAS将cellsBusy设置为1才成功创建。

重点:如何给线程在cells中定位Cell?

是利用 getProbe() 方法,这个方法会返回当前线程的 probe值,probe 值如下:

PROBE = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomProbe"));

4、AtomicReference

在 atomic 包下基本都提供了基本类型对应的 atomic 类,如 AtomicInteger、AtomicLong、AtomicBoolean 等。但是引用肯定就无法直接提供对应的 Atomic 类了,所以 JDK 提供了 AtomicReference。

核心属性是:private volatile V value; 底层是利用 Unsafe 的 boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5) 方法。

  • var1:当前 AtomicReference 对象
  • var2:value 在 AtomicReference 对象中的 offset
  • var4:预期值
  • var5:设置值

注意:AtomicReference 和 volatile 一样,volatile 保证的是对象引用的可见性,AtomicReference 保证的是对象引用的并发安全,至于对象里面的内容,无法保证。

5、AtomicStampedReference

CAS 有两个比较明显的缺点:

  • 轮询问题:这个没办法,可以考虑像 LongAdder 一样利用 Cell[] 来优化 AtomicLong
  • ABA问题:线程1获取预期值时是A,线程2改成B,接着线程3又改回A,所以当线程1使用CAS更新时还是会成功,但其实此时的预期值已经被修改过两次。

针对 ABA 问题,JDK 提供了 AtomicStampedReference,它在 AtomicReference 基础上添加了 stamp,即邮戳的概念,每次进行 CAS 操作
同时要传入stamp的预期值和value的预期值。

AtomicStampedReference 是利用内部类 Pair 来保存stamp和value,而属性 pair 也加了 volatile 关键字来保证可见性。

public class AtomicStampedReference<V> {

    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;
    
    private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
    private static final long pairOffset =
        objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);

    
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        // reference 预期值符合 && stamp预期值符合 &&(reference和stamp预期值都==设置值 || CAS 设置pair成功) 
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
    
    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }
    
    // ....    
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值