Java原子类

一、CAS

什么是CAS,CAS就是Compare and Swap

CAS是一种无锁算法

原理:

对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

举个例子,表示一下CAS的原理。假设t1在与t2线程竞争中线程t1能去更新变量的值,而其他线程都失败。(失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试)。t1线程去更新变量值改为57,然后写到内存中。此时对于t2来说,内存值变为了57,与预期值56不一致,就操作失败了(想改的值不再是原来的值)。

代码:

public final int getAndAddInt(Object var1, long var2, int var4) {
    //1 操作的对象,2 对象中字段的偏移量,3 原来的值,4 要修改的值。
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);//得到当前的值。
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

上面的代码是原子类中的一个例子。

ABA问题:

CAS乐观锁机制确实能够提升吞吐,并保证一致性,但在极端情况下可能会出现ABA问题。就是期望值被从A改为B,又从B改为A.CAS还能成功。

解决方法:加版本号。

二、原子类

原子类,顾名思义就是不可分割的,当原子类去执行操作的话是一步合成的。

以前的时候我们并发操作的时候,回去选择加锁。加锁还是有很多影响的。比如:性能损耗、逻辑复杂。所以在JDK1.5的时候Doug Lea写了原子类,也是在JUC包下。

1、原子类介绍

1.1、原子更新基本类型

下面是三种基本原子类型,以及他们常用的API。

AtomicInteger atomicInteger = new AtomicInteger(1);
        atomicInteger.getAndAdd(1); //以原子方式将给定值添加到当前值。
        atomicInteger.compareAndSet(1,2); //如果当前值==期望值,则以原子方式将该值设置为给定的更新值。
        atomicInteger.get(); //获取当前值
        atomicInteger.getAndIncrement(); //以原子方式将当前值增加一。
        atomicInteger.getAndDecrement(); //以原子方式将当前值减一。
        atomicInteger.addAndGet(2); //以原子方式将给定值添加到当前值。
        atomicInteger.weakCompareAndSet(1,2); //如果当前值==期望值,则以原子方式将该值设置为给定的更新值。可能会虚假地失败,并且不提供排序保证,因此,很少是compareAndSet的适当替代方法。
        AtomicBoolean atomicBoolean = new AtomicBoolean();
        atomicBoolean.lazySet(true); //最终设置为给定值。
        AtomicLong atomicLong = new AtomicLong(1L);
        atomicLong.addAndGet(2L); //以原子方式将给定值添加到当前值。
        //基本和Integer相似,就不一一列举了。

总的来说,它们的底层都使用了CAS,我们来找一个看看。

atomicInteger.addAndGet(2);

//第一层
public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + 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)); //var4是我们想要增加的值
        return var5; //注意这个var5是加var4之前的值。
    }

1.2、原子更新数组

我们再来看一下原子更新数组的一些基本操作

		AtomicIntegerArray array = new AtomicIntegerArray(16);
        array.set(1,16); //将位置i的元素设置为给定值。
        array.addAndGet(1,13); //以原子方式将给定值添加到索引i处的元素。
        System.out.println(array.get(1)); //获取位置i的当前值。
        AtomicLongArray longArray = new AtomicLongArray(16);
        // 基本和AtomicIntegerArray的API相似
        longArray.set(1,2);

我们看一下AtomicIntegerArray的内部原理

public AtomicIntegerArray(int length) {   //我们看一下初始化方法,内部就是维护了一个private final int[] array;
        array = new int[length];
    }

看一下array.addAndGet(1,13);这个方法的内部原理。

//第一层
public final int addAndGet(int i, int delta) {
        return getAndAdd(i, delta) + delta;
    }
//第二层
public final int getAndAdd(int i, int delta) {
        return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
    }

private long checkedByteOffset(int i) {
        if (i < 0 || i >= array.length)
            throw new IndexOutOfBoundsException("index " + i);

        return byteOffset(i);
    }
//第三层
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;
    }
//还是调用的这个方法。

AtomicReferenceArray引用数组的基本使用方法。

		AtomicReferenceArray referenceArray = new AtomicReferenceArray(16);
        referenceArray.set(1, "222");
        //将给定函数应用于当前值和给定值,以原子方式更新索引i处的元素,并返回更新后的值。
        // 该功能应无副作用,因为当尝试更新由于线程之间的争用而失败时,可以重新应用该功能。
        // 应用该函数时,将索引i处的当前值作为其第一个参数,并将给定的update作为第二个参数。
        referenceArray.accumulateAndGet(1, "999", (x, y) -> x.toString() + y);
//输出结果为222999

我们看一下这个方法内部原理。

public final E accumulateAndGet(int i, E x,
                                    BinaryOperator<E> accumulatorFunction) {
        long offset = checkedByteOffset(i); //获取下标在数组中的偏移量
        E prev, next;
        do {
            prev = getRaw(offset);  //获取当前的值
            next = accumulatorFunction.apply(prev, x);  //获取函数执行后的值
        } while (!compareAndSetRaw(offset, prev, next)); //CAS赋值。
        return next;
    }

1.3、原子更新属性、引用。

原子的更新某个类里面的字段时,需要使用原子更新字段类。

我们来看看有一个原子类型。

AtomicStampedReference、AtomicReference、FiledUpdater。

不过使用上述几个引用,有几个规则必须遵守

  • 字段必须是voliatile类型的,在线程之间共享变量时能保持可见性。
  • 字段的描述类型是与调用者的操作对象字段保持一致。
  • 也就是说调用者可以直接操作对象字段,那么就可以反射进行原子操作。
  • 对父类的字段,子类不能直接操作的,尽管子类可以访问父类的字段。
  • 只能是实例变量,不能是类变量,也就是说不能加static关键字。
  • 只能是可修改变量,不能使用final修饰变量,final的语义,不可更改。
  • 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)

AtomicLongFieldUpdater这个只对long进行更新的使用方法。

AtomicLongFieldUpdater<Son> updater = AtomicLongFieldUpdater.newUpdater(Son.class, "id");
        Son son = new Son();
        son.setId(9);
        updater.addAndGet(son,7);

如果我们想更改包装类,就使用AtomicReferenceFieldUpdater

AtomicReferenceFieldUpdater<Son,String> fieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Son.class,String.class,"name");
        fieldUpdater.compareAndSet(son,"777","666");

AtomicStampedReference 前面我们提到了ABA问题并且提到了解决办法就是加版本号,这个原子类就是带版本号的。

AtomicStampedReference<String> stampedReference = new AtomicStampedReference("aaa", 1);
        stampedReference.compareAndSet("aaa", "bbb", stampedReference.getStamp(), stampedReference.getStamp() + 1);
   

小结

我们去看原子类内部时,发现最终都是使用了CAS的方法。原子类就是在CAS发展来的。

posted @ 2021-01-11 22:01  红警贼秀  阅读(125)  评论(0编辑  收藏  举报