第二章 Atomic类

本文深入探讨Java中的原子类,包括AtomicInteger、AtomicLong等的实现原理,乐观锁与悲观锁的区别,ABA问题及其解决方案,以及AtomicStampedReference和AtomicMarkableReference的特性。同时,文章还介绍了AtomicXXXFieldUpdater的作用,AtomicIntegerArray等数组原子操作类,以及Striped64与LongAdder的高效并发设计。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2.1 AtomicInteger和AtomicLong

AtomicInteger和AtomicLong可以实现自增或者自减的原子操作。

AtomicInteger jdk 1.8源码如下:

AtomicInteger类中的方法

    private static final long VALUE;
    //内存偏移量
    static {
        try {
            VALUE = U.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }

    public final int getAndIncrement() {
        return U.getAndAddInt(this, VALUE, 1);
    }


Unsafe类中的方法
    //var5 旧值 var5+var4 新值
    //var2 内存偏移量,后面cas的操作是对内存偏移量的操作
    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;
    }

2.1.1 悲观锁与乐观锁

悲观锁:认为并发冲突的概率很大,所以读操作之前就上锁。synchronized(jdk1.6之前)和ReentrantLock都是悲观锁。

乐观锁:认为并发冲突概率比较小,所以读操作之前不上锁。等到写操作的时候,在判断数据在此期间是否被其他线程修改。如果被修改则读出来,重复该过程。如果没有被修改,就写回去。

CAS:判断数据是否被修改,同时写回新值,这两个操作合成了一个原子操作,也就是CAS(Compare And Set)。

AtomicInteger和AtomicLong是典型的乐观锁。

2.1.2 Unsafe的CAS详解

var5是旧值,var5+var4是新值,当var5等于当前的值时,说明在修改期间没有其他线程对此变量进行修改,所以可以写入,返回true。否则不能写入,返回false。

var2是内存偏移量,后面的cas操作都是对var2的操作。

2.1.3 自旋与阻塞

当线程拿不到锁的时候有两个等待策略。

阻塞:放弃cpu,进入阻塞状态,等待后续被唤醒。

自旋:不放弃cpu,空转,不断重试。

2.2 AtomicBoolean和AtomicReference

2.2.1 AtomicBoolean解决下面问题:

if(!flag){
    flag=true;
}

上述不是原子操作,所以需要AtomicBoolean完成,操作如下:

AtomicBoolean类中    
    public final boolean compareAndSet(boolean expect, boolean update) {
        return U.compareAndSwapInt(this, VALUE,
                                   (expect ? 1 : 0),
                                   (update ? 1 : 0));
    }

2.2.2 AtomicReference:对象引用变量

2.2.3 如何支持boolean和double类型

boolean转换成int类型(在AtomicBoolean类中),double转换成long类型(注意double没有Atomic类,是Double自身提供的方法)

2.3 ABA问题解决方案

2.3.1 ABA问题与解决方法

①.问题:前面说的cas操作都是基于“值”来比较的,但是一个线程把变量的值从A改成B,再改成A,其他线程却因为值的没有改变来判定没有被修改过。

②.解决方案:不仅要比较“值”,还要比较“版本号”。

AtomicStampedReference就是先比较值在比较版本号。

2.3.2 为什么没有AtomicStampedInteger和AtomicStampedLong

因为是同时比较数据的“值”和“版本号”,然后封装成一个对象,是Pair内部类,然后通过对象引用的cas来实现,因为Integer和Long无法同时比较两个变量值,所以不存在AtomicStampedInteger和AtomicStampedLong,如下是AtomicStampedReference源码

    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)));
    }

2.3.3 AtomicMarkableReference:版本号用boolean类型的,相对AtomicStampedReference存在风险

2.4 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater

解决在类的成员变量不是Atomic类型情况下,并且不修改源码的情况下,用上面三种方式可以实现原子操作

原理:内部通过反射拿到这个类的成员变量,然后包装成AtomicXXXFieldUpdater类

限制条件:使用AtomicXXXFieldUpdater修改的成员变量必须是有volatile修饰的 

2.5 AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray

作用:数组元素的原子操作,注意不是整个数组的原子操作,而是数组中一个元素的原子操作

用法:相比AtomicXXX多一个数组下标参数

2.6 Striped64与LongAdder

Striped64子类有:LongAdder、LongAccumulator、DoubleAdder、DoubleAccumulator

2.6.1 LongAdder原理:将变量拆成多份,分别是一个base变量和多个Cell,如果并发量低对base进行操作,如果并发量高对Cell进行操作,相对于AtomicXXX更加高效,提高性能,如下图:

源码如下:

abstract class Striped64 extends Number {
    transient volatile Striped64.Cell[] cells;
    transient volatile long base;
    ...
    //此处Contended是缓存行填充,后面会介绍
    @Contended
    static final class Cell {
        volatile long value;
    ...
    }
}

2.6.2 最终一致性

如上方式是完成了最终一致性操作,不是强一致性。因为一边有现成在执行求和操作,一边有线程在修改数组里的数据。

2.6.3 伪共享与缓存行填充

jdk 8重要的注解@Contended涉及一个很重要的优化原理:伪共享与缓存行填充

如下变量X,Y,Z缓存在cpu里面同一行,如果X变量被修改导致失效,则Y,Z都失效,这就是伪共享。

加了@Contended注释就是具有缓存行填充,可以防止Y,Z失效。JDK 1.7也有,不过不是注释的形式。

2.6.4 LongAdder 核心实现

①.LongAdder最核心的是累加函数add(long x),自增自减都是最终调用这个函数。源码如下

    public void add(long var1) {
        Cell[] var3;
        long var4;
        if ((var3 = this.cells) != null || !this.casBase(var4 = this.base, var4 + var1)) {//第一次尝试加到base上面
            boolean var10 = true;
            long var6;
            int var8;
            Cell var9;
            if (var3 == null || (var8 = var3.length - 1) < 0 || (var9 = var3[getProbe() & var8]) == null || !(var10 = var9.cas(var6 = var9.value, var6 + var1))) {//第二次尝试加到cell上面
                this.longAccumulate(var1, (LongBinaryOperator)null, var10);
            }
        }

    }

如上所示,第一次尝试加到base上,如果不成功则第二次尝试加到cell上,如果不成功则调用longAccumulate方法。

longAccumulate的代码较长,里面主要做的事是:初始化数组,对数组进行扩容,每次扩容是原先的两倍,然后将新的数据加到扩容的数组中

2.6.5 LongAccumulator

原理和LongAdder类似,功能更强大

LongAdder只能累加操作,并且初始化值默认是0,LongAccumulator可以自定义一个二元操作符,并且可以传入一个初始值。

2.6.6 DoubleAdder与DoubleAccumulator

原理:转换成long进行操作的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龚礼鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值