CAS与AtomicInteger及LongAdder浅析

目录

CAS

概念

CAS举例

CAS过程分析

CAS原理分析

CAS性能分析

CAS特点

AtomicInteger

AtomicReference

CAS的ABA问题

问题描述

解决办法

LongAdder

性能对比

原理分析

源码分析

成员变量

Cell累加单元

add()

longAccumulate()

sum()


CAS

概念

Java中,锁占了并发的一席之地,但是锁带来的弊端就是线程会频繁的阻塞挂起,导致上下文的切换和重新调度,增加了系统开销。CAS 即 Compare and Swap,是 JDK 提供的非阻塞原子性操作 , 它通过硬件保证了比较更新操作的原子性 ,有效减小了因为上下文切换导致的开销问题。

CAS举例

下面代码分别演示了多线程下普通方式(非线程安全)以及利用CAS方式操作变量

-- 普通方式
public class CasTest01 extends Thread{
    private static int count = 1000;
    private static AtomicInteger anInt = new AtomicInteger(1000);
    public static void main(String[] args) {
        CasTest01 t1 = new CasTest01();
        CasTest01 t2 = new CasTest01();
        CasTest01 t3 = new CasTest01();
        t1.setName("线程1");
        t2.setName("线程2");
        t3.setName("线程3");
        t1.start();
        t2.start();
        t3.start();
    }
    @Override
    public void run() {
        while (true){
            if(count>1){
                try {
                    Thread.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println(Thread.currentThread().getName()+":"+count);
            }else {
                break;
            }
        }

    }
}
-- 输出
线程1:1
线程3:0
线程2:-1

改为compareAndSet方式后
public void run() {
        while (true){
            for (;;){
                if(anInt.get()>=1){
                    int prev = anInt.decrementAndGet();
                    int next = prev - 1;
                    // 如果失败则重试
                    if(anInt.compareAndSet(prev, next)){
                        System.out.println(Thread.currentThread().getName()+":"+anInt.get());
                        break;
                    }
                }
            }
        }

    }

--输出
线程1:2
线程1:1
线程1:0
线程2:15
线程3:11

可以看到,如果两个线程同时操作同一变量,不加以控制则产生了线程安全问题,采用CAS的方式,有效保证了线程安全,未出现 -1的情况。

CAS过程分析

假设多线程操作的变量是个银行账户,每个线程依次扣除余额,CAS过程如下

public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
expect:槽位预期值
update:更新后值

结合上图及compareAndSet方法,执行compareAndSet方法时,只要expect与当前值(预计槽位值)不一致,则方法返回false,更新失败后,根据具体业务进行下一步处理。

CAS原理分析

CAS底层使用的是lock cmpxchg指令,在单核CPU和多核CPU下都能保证【比较-交换】的原子性。在多核状态下,某个核执行到带lock的指令时,CPU会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制锁打断,保证了多个线程内部操作的准确性 和原子性。CAS必须借助volatile才能读取到共享变量的最新值来实现【比较并交换】的效果。

CAS性能分析

在无锁情况下,即使重试失败,线程始终在高速运行,没有停止,而synchronized会让线程在没有获取锁的时候,发生上下文切换,进入阻塞。

但在无锁情况下,因为线程要保持运行,需要额外的CPU支持,线程虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致线程的上下文切换。

CAS特点

结合CAS和volatile可以实现无锁并发,适用于线程数少,多核CPU场景下,

CAS是基于乐观锁的思想:不怕别的线程来修改共享变量,

CAS体现的是无锁并发、无阻塞并发   

     因为没有使用sychronized,所以线程不会陷入阻塞,这是提升效率的主要原因

    但如果竞争激烈,重试必然频繁发生,反而效率会降低。

AtomicInteger

除了compareAndSet方法,AtomicInteger还提供了更简洁的加减法操作,底层都是通过CAS的方式实现。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

AtomicReference

如果操作的不是Integer类型,Java中还提供了原子引用类型AtomicReference,结合泛型使用,用法如下:

AtomicReference<BigDecimal> atomicReference = new AtomicReference<>();

BigDecimal before = atomicReference.get();

BigDecimal next = before.subtract(BigDecimal.ONE);

atomicReference.compareAndSet(before, next);

CAS的ABA问题

问题描述

 假如线程 I 使用 CAS 修改初始值为 A 的变量 X , 那么线程 I 会首先去获取当前变量 X 的值(为 A 〕 , 然后使用 CAS 操作尝试修改 X 的值为 B , 如果使用 CAS 操作成功 了 , 那么程序运行一定是正确的 吗 ?其实未必,这是因为有可能在线程 I 获取变量 X 的值 A 后,在执行 CAS 前,线程 II 使用 CAS 修改了变量 X 的值为 B ,然后又使用 CAS 修改 了 变量 X 的值为 A 。 所以 虽然线程 I 执行 CAS时 X 的值是 A , 但是这个 A 己经不是线程 I 获取时的 A 了 。 这就是 ABA 问题 。

即线程仅能判断出共享变量的值与最初值A是否相同,不能感知从A改为B又改回A的情况。

解决办法

JDK 中 的 AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳 , 从而避免 ABA 问题的产生。具体如下:

public class CasTest03 {

    static AtomicStampedReference<String>  ref = new AtomicStampedReference<>("A",0);
    public static void main(String[] args) throws Exception {
        // 初始值
        String prev = ref.getReference();
        // 版本号
        int stamp = ref.getStamp();

        System.out.println("主线程版本号:"+stamp);
        System.out.println();
        other();
        Thread.sleep(3);
        System.out.println("主线程change A->C:"+ref.compareAndSet(prev,"C",stamp,stamp+1));
        
    }

    private static void other() throws InterruptedException {
        new Thread(()->{
            int stamp = ref.getStamp();
            System.out.println("线程1版本号:"+stamp);

            // A update为B
            System.out.println("线程1change A->B:"+ref.compareAndSet(ref.getReference(),"B",stamp,stamp+1));
            System.out.println();
        }).start();
        Thread.sleep(1);
        new Thread(()->{
            int stamp = ref.getStamp();
            System.out.println("线程2版本号:"+stamp);
            // A update回B
            System.out.println("线程2 change B->A:"+ref.compareAndSet(ref.getReference(),"A",stamp,stamp+1));
            System.out.println();
        }).start();
    }
}

-- 输出

主线程版本号:0

线程1版本号:0
线程1change A->B:true

线程2版本号:1
线程2 change B->A:true

主线程change A->C:false
  • main方法中获取了了引用类型的当前值和版本号,期望在当前版本下,将A变为C
  • other方法中启动了两个线程分别进行A->B 及B->A的更新
  • 主线程中执行更新操作发现,虽然期望值仍为A,但是版本号已经是2(说明其他线程变更过),即更新失败,成功的感知到了其他线程的修改操作

LongAdder

性能对比

AtomicInteger中虽然提供了incrementAndGet方法进行了原子性累加,但性能并不是最好的,大师Doug Lea在Java 1.8中提供了LongAdder类,进一步提升了运算性能。AtomicInteger与LongAdder性能对比如下:

public class CasTest04 {
    
   
    public static void main(String[] args) throws InterruptedException {
        //分别开4个线程累加50000次
        testAtomicInteger();
        testLongAddr();
    }
    // AtomicInteger
    private static void testAtomicInteger() throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger();

        long start = System.currentTimeMillis();
        List<Thread> list = new ArrayList<>();
        for (int loop = 0; loop <= 4; loop++) {
            Thread t = new Thread(() -> {
                for (int i = 0; i < 50000; i++) {
                    atomicInteger.incrementAndGet();
                }
            });
            list.add(t);
        }
        list.forEach(t -> t.start());
        for (Thread t : list) {
            t.join();
        }
        long end = System.currentTimeMillis();
        System.out.println("testAtomicInteger cost:"+ (end-start));
    }

    // LongAddr
    private static void testLongAddr() throws InterruptedException {
        LongAdder longAdder = new LongAdder();
        long start = System.currentTimeMillis();
        List<Thread> list = new ArrayList<>();
        for (int loop = 0; loop <= 4; loop++) {
            Thread t = new Thread(() -> {
                for (int i = 0; i < 50000; i++) {
                    longAdder.increment();
                }
            });
            list.add(t);
        }
        list.forEach(t -> t.start());
        for (Thread t : list) {
            t.join();
        }

        long end = System.currentTimeMillis();
        System.out.println("testLongAddr cost:"+ (end-start));
    }
}

-- 输出
testAtomicInteger cost:58
testLongAddr cost:7

可以看到,代码中分别用AtomicInteger 和LongAdder完成了相同数量的叠加操作,执行效率上,LongAddr明显更高。

原理分析

使用 AtomicLong 时,在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的CAS 操作会成功,这就造成了大量线程竞争失败后,会通过无限循环不断进行自旋尝试CAS 的操作, 而这会白白浪费 CPU 资源。LongAdder核心思想是把一个变量分解为多个变量,让同样多的线程去竞争多个资源 ,减小竞争压力。原理对比如下:

源码分析

成员变量

     // 累加单元数组, 懒惰初始化
     transient volatile Cell[] cells;

     // 基础值,如果没有竞争,则用cas累加这个值
     transient volatile long base;

     // 在cells创建或扩容时,置为1,表示加锁 
     transient volatile int cellsBusy;

Cell累加单元

// 防止缓存行伪共享
@sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        // 最重要的方法,用来cas方式进行累加,prev表示旧值,next表示新值
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }
     ....
    }
  • @sun.misc.Contended  作用是防止缓存行伪共享

伪共享:因为CPU与内存的速度差异很大,需要靠预读数据至缓存来提升效率,缓存是以缓存行为单位,每个缓存行对应着一块内存,一般是64bytes(8个long),缓存行的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU要保证数据的一致性,如果某个CPU核心更改了数据,其他CPU对应的整个缓存行必须失效。

因为Cell是数组形式,在内存中是连续存储的,一个Cell为24个字节(16字节的对象头和8字节的value),因此缓存行可以存下2个Cell对象,问题如下:

Core-0要修改Cell[0],Core-1也要修改Cell[1],无论谁修改成功,都会导致对方Core的缓存行失效。不得不再次从主内存中加载数据,严重影响性能。

@sun.misc.Contended用来解决上述问题,原理是使用了该注解的对象或字段前后各增加128字节的大小的padding,从而让CPU将对象预读至缓存时占用不同的缓存行,这样不会造成对方缓存行的生效。如下所示:

add()

public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        // 如果初始cells为空(即无竞争),则执行cas操作累计操作
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                // 取当前线程对应槽位,如果没有创建过则执行longAccumulate
                // 如果创建过,则取出对应槽位值进行累加,失败则执行longAccumulate
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }

对应流程图如下:

longAccumulate()

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) {
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            // 数组创建好了
            if ((as = cells) != null && (n = as.length) > 0) {
                //图2 但是对应的累加单元还没创建好
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        // 创建一个Cell对象
                        Cell r = new Cell(x);   // Optimistically create
                        // 上锁
                        if (cellsBusy == 0 && casCellsBusy()) {
                            // 加锁成功
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    // 如果槽位上数据为空,则将新Cell元素放入槽位
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                // 解锁
                                cellsBusy = 0;
                            }
                            // Cell创建成功且放入槽位则退出循环
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                // 图3 数组创建好,累加单元也存在 尝试CAS对累加单元累加
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                // 如果通过CAS累加失败,则判断n是否超出CPU
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        if (cells == as) {      // Expand table unless stale
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                // 改变线程所对应的cell
                h = advanceProbe(h);
            }
            // 图1 还没有其他线程扩容或者创建cells && cells还没有被其他线程所修改 && cas cellsBusy变量0->1成功
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                        // 新建Cell数字
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            // 对base进行累加
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

图1、数组不存在:

图2、数组创建好,但是线程没有对应的累加单元:

图3、数组创建好,有累加单元

sum()

即累加base及Cells元素作为最后的结果

public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

参考资料:《Java并发编程之美》

                   《Java高并发编程详解》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值