并发编程005 --- 原子类的使用和原理

原子操作是指不会被线程调度机制打断的操作,也就是说在原子操作期间,不会出现线程上下文切换;

JDK在java.util.concurrent.atomic包中提供了多个原子类,如下:

 

其中从DoubleAccumulator开始,是JDK1.8提供的采用分段思想的高性能原子类;

在多线程场景中,不可避免的会有数据的加减运算,很显然这些操作不是线程安全的;我们可以通过synchronized、Lock等方式保证线程安全,但是这些加锁操作大多数情况下会降低执行效率;另外的一种方法就是采用CAS + 自旋锁来解决线程安全问题

CAS ---  Compare And Swap,先去内存中获取当前变量值,如果是预期的值,更新为新的值;

自旋锁 ---- 当线程尝试获取锁失败时,该线程可以阻塞,等待所释放后OS的调度,还可以执行一个空循环,不断的占用当前CPU,尝试获取锁,后面一种即是自旋锁;但是其存在一些弊端,如果自旋锁执行时间过长,会导致CPU资源长时间被占用,而且该资源消耗会大于线程上下文切换,因此,需要根据场景区分使用咨自旋锁;

上述原子类底层即采用了CAS + 自旋锁来解决线程安全问题

下面以AtomicInteger的常用方法为例说明下原子类的基本用法:

public static void main(String[] args) {
    final AtomicInteger atomicInteger = new AtomicInteger(1);
    System.out.println(atomicInteger.incrementAndGet());
}

毫无疑问,运行结果会是2,进入incrementAndGet方法源码,了解下原子类底层原理

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    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;
    }

这里面有个很重要的变量:valueOffset,指的是变量在内存中的偏移量;

getAndAddInt方法的流程为,先从主内存中读取变量值,然后调用CAS方法,直到内存中的变量值为预期值;

CAS算法的思想就是,先从内存中取出某个时刻的值A,在下一个时刻将该值与内存中的值相比较,如果没有变化,则进行更新;那么在时间差内,其它的线程可能将内存中的值先由A修改为B,然后再修改为A;原来的线程在比较时,发现取值没有发生变化,进行了更新;这就是CAS的“ABA”问题;

正常的运算场景下,ABA不会引入业务问题,但是某些场景下,ABA会导致问题,比如:

单向链表A->B构成的栈,A是栈顶,现在要栈顶更新为B,前提是栈顶为A

1、线程1,先获取栈顶的值,为A;

2、这时线程2将A、B出栈,然后在栈中分别入栈D、C、A,此时栈结构如下: A->C->D

3、线程1,执行比较,发现栈顶为A,因此更新栈顶为B,但是此时B的next为空,偏离了预期

为了解决ABA问题,java引入了带“版本号”的原子类AtomicMarkableReference和AtomicStampedReference,如果出现ABA的场景,那么变量的版本号加1,并且在比较时认为不是同一个值

posted @ 2020-08-19 21:54  光头用沙宣  阅读(200)  评论(0编辑  收藏  举报