• 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发展来的。

  • 相关阅读:
    Serverless 工程实践 | Serverless 应用开发观念的转变
    如何高效学习 Kubernetes 知识图谱?
    互动赠新书|当云原生遇到混合云:如何实现“求变”与“求稳”的平衡
    5 款阿里常用代码检测工具,免费用!
    AI与传统编译器
    OpenArkCompiler方舟编译
    传统编译原理
    LLVM基础技术图例
    双极型与低频大功率晶体管
    TVM,Relay,Pass
  • 原文地址:https://www.cnblogs.com/kenai/p/14264638.html
Copyright © 2020-2023  润新知