Java并发编程工具类篇2-原子操作类之18罗汉增强(LongAdder源码)

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

1. 基本介绍

包:java.util.concurrent.atomic

阿里开发手册中:

  • 【参考】volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。
  • 说明:如果是 count++ 操作,使用如下类实现:Atomiclnteger count=new AtomicInteger();count.addAndget(1)如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)

常见的原子类如下:

  • AtomicBoolean
  • Atomiclnteger
  • AtomiclntegerArray
  • AtomiclntegerFieldUpdater
  • AtomicLong
  • AtomicLongArray
  • AtomicLongFieldUpdater
  • AtomicMarkableReference
  • AtomicReference
  • AtomicReferenceArray
  • AtomicReferenceFieldUpdater
  • AtomicStampedReference
  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

2. 基本类型原子类

基本类型的原子类有三个如下:AtomicBoolean、Atomiclnteger、AtomicLong

常用API如下:

  • public final int get() ∥获取当前的值
  • public final int getAndSet(int newValue) //获取当前的值,并设置新的值
  • public final int getAndIncrement() //获取当前的值,并自增
  • public final int getAndDecrement() ∥获取当前的值,并自减
  • public final int getAndAdd(int delta) ∥获取当前的值,并加上顶期的值
  • boolean compareAndSet((int expect,int update) ∥如果输入的数值等于预期值,则以原子方式将该值设置为输入值

简单例子如下:

class MyNumber {
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addPlusPlus() {
        atomicInteger.incrementAndGet();
    }
}

public class AtomicIntegerDemo {
    public static final int SIEZ_ = 50;

    public static void main(String[] args) throws InterruptedException {

        MyNumber myNumber = new MyNumber();

        for (int i = 1; i <= SIEZ_; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myNumber.addPlusPlus();
                }
            }, String.valueOf(i)).start();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "---result : " + myNumber.atomicInteger.get());
    }
}
复制代码

上述代码执行结果为什么不是50*1000?因为50个线程还没有执行完,Main线程就要获取结果,所以结果不正确,但程序是正确的。

使用CountDownLatch解决如下:

class MyNumber {
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addPlusPlus() {
        atomicInteger.incrementAndGet();
    }
}

public class AtomicIntegerDemo {
    public static final int SIEZ_ = 50;

    public static void main(String[] args) throws InterruptedException {

        MyNumber myNumber = new MyNumber();
        CountDownLatch countDownLatch = new CountDownLatch(SIEZ_);

        for (int i = 1; i <=SIEZ_; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1 ;j <=1000; j++) {
                        myNumber.addPlusPlus();
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    countDownLatch.countDown();
                }
            },String.valueOf(i)).start();
        }

        //try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        countDownLatch.await();//让main线程阻塞,即得等到上面的线程全部执行完成

        System.out.println(Thread.currentThread().getName()+"\t"+"---result : "+myNumber.atomicInteger.get());
    }
}
复制代码

3. 数组类型原子类

AtomiclntegerArray、AtomicLongArray、AtomicReferenceArray

简单使用代码演示如下:

public class AtomicIntegerArrayDemo {
    public static void main(String[] args) {
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
        //AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(5);
        //AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1,2,3,4,5});

        for (int i = 0; i <atomicIntegerArray.length(); i++) {
            System.out.println(atomicIntegerArray.get(i));
        }
        System.out.println();
        System.out.println();
        System.out.println();
        int tmpInt = 0;

        tmpInt = atomicIntegerArray.getAndSet(0,1122);
        System.out.println(tmpInt+"\t"+atomicIntegerArray.get(0));
        atomicIntegerArray.getAndIncrement(1);
        atomicIntegerArray.getAndIncrement(1);
        tmpInt = atomicIntegerArray.getAndIncrement(1);
        System.out.println(tmpInt+"\t"+atomicIntegerArray.get(1));

    }
}
复制代码

4. 引用类型原子类

AtomicReference
AtomicReference简单使用的代码演示如下:

@Getter
@ToString
@AllArgsConstructor
class User {
    String userName;
    int    age;
}

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User z3 = new User("z3",24);
        User li4 = new User("li4",26);

        AtomicReference<User> atomicReferenceUser = new AtomicReference<>();

        atomicReferenceUser.set(z3);
        System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t"+atomicReferenceUser.get().toString());
        System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t"+atomicReferenceUser.get().toString());
    }
}
复制代码

AtomicStampedReference :携带版本号的引用类型原子类,可以解决ABA问题,解决修改过几次的问题,ABA问题的代码演示如下:

public class ABADemo {
    static AtomicInteger atomicInteger = new AtomicInteger(100);
    static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);

    public static void main(String[] args) {
        abaProblem();
        abaResolve();
    }

    public static void abaResolve() {
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println("t3 ----第1次stamp  "+stamp);
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            atomicStampedReference.compareAndSet(100,101,stamp,stamp+1);
            System.out.println("t3 ----第2次stamp  "+atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println("t3 ----第3次stamp  "+atomicStampedReference.getStamp());
        },"t3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println("t4 ----第1次stamp  "+stamp);
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean result = atomicStampedReference.compareAndSet(100, 20210308, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+"\t"+result+"\t"+atomicStampedReference.getReference());
        },"t4").start();
    }

    public static void abaProblem() {
        new Thread(() -> {
            atomicInteger.compareAndSet(100,101);
            atomicInteger.compareAndSet(101,100);
        },"t1").start();

        try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            atomicInteger.compareAndSet(100,20210308);
            System.out.println(atomicInteger.get());
        },"t2").start();
    }
}
复制代码

AtomicMarkableReference : 原子更新带有标记位的引用类型对象,解决是否修改过,它的定义就是将状态戳简化为true|false,状态戳(true/false)原子引用代码演示如下:

public class ABADemo {
    static AtomicInteger atomicInteger = new AtomicInteger(100);
    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
    static AtomicMarkableReference<Integer> markableReference = new AtomicMarkableReference<>(100,false);

    public static void main(String[] args) {
        new Thread(() -> {
            atomicInteger.compareAndSet(100,101);
            atomicInteger.compareAndSet(101,100);
            System.out.println(Thread.currentThread().getName()+"\t"+"update ok");
        },"t1").start();

        new Thread(() -> {
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            atomicInteger.compareAndSet(100,2020);
        },"t2").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println(atomicInteger.get());

        System.out.println();
        System.out.println();
        System.out.println();

        System.out.println("============以下是ABA问题的解决,让我们知道引用变量中途被更改了几次=========================");
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t 1次版本号"+stampedReference.getStamp());
            //故意暂停200毫秒,让后面的t4线程拿到和t3一样的版本号
            try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }

            stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 2次版本号"+stampedReference.getStamp());
            stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 3次版本号"+stampedReference.getStamp());
        },"t3").start();

        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t =======1次版本号"+stamp);
            //暂停2秒钟,让t3先完成ABA操作了,看看自己还能否修改
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean b = stampedReference.compareAndSet(100, 2020, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+"\t=======2次版本号"+stampedReference.getStamp()+"\t"+stampedReference.getReference());
        },"t4").start();

        System.out.println();
        System.out.println();
        System.out.println();

        System.out.println("============AtomicMarkableReference不关心引用变量更改过几次,只关心是否更改过======================");

        new Thread(() -> {
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"\t 1次版本号"+marked);
            try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
            markableReference.compareAndSet(100,101,marked,!marked);
            System.out.println(Thread.currentThread().getName()+"\t 2次版本号"+markableReference.isMarked());
            markableReference.compareAndSet(101,100,markableReference.isMarked(),!markableReference.isMarked());
            System.out.println(Thread.currentThread().getName()+"\t 3次版本号"+markableReference.isMarked());
        },"t5").start();

        new Thread(() -> {
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"\t 1次版本号"+marked);
            //暂停几秒钟线程
            try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
            markableReference.compareAndSet(100,2020,marked,!marked);
            System.out.println(Thread.currentThread().getName()+"\t"+markableReference.getReference()+"\t"+markableReference.isMarked());
        },"t6").start();
    }
}
复制代码

5. 对象的属性修改原子类

使用目的:以一种线程安全的方式操作非线程安全对象内的某些字段。使用要求:

  • 更新的对象属性必须使用 public volatile 修饰符
  • 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。

举例子:以前,卖票,

  • 多对一,多个线程操作同一个票资源类,加synchronized,重,正确
  • 多对一,多个线程操作同一个系统类,不加synchronized,微创,只对某个字段进行原子操作保护,轻,正确

常用类:

  • AtomiclntegerFieldUpdater : 原子更新对象中int类型字段的值
  • AtomicLongFieldUpdater:原子更新对象中Long类型字段的值
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的值

AtomicIntegerFieldUpdaterDemo类的简单使用代码演示如下:

class BankAccount {
    private String bankName = "CCB";//银行
    public volatile int money = 0;//钱数
    AtomicIntegerFieldUpdater<BankAccount> accountAtomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"money");

    //不加锁+性能高,局部微创
    public void transferMoney(BankAccount bankAccount) {
        accountAtomicIntegerFieldUpdater.incrementAndGet(bankAccount);
    }
}

/**
 * 以一种线程安全的方式操作非线程安全对象的某些字段。
 * 需求:
 * 1000个人同时向一个账号转账一元钱,那么累计应该增加1000元,
 * 除了synchronized和CAS,还可以使用AtomicIntegerFieldUpdater来实现。
 */
public class AtomicIntegerFieldUpdaterDemo {

    public static void main(String[] args) {
        BankAccount bankAccount = new BankAccount();

        for (int i = 1; i <=1000; i++) {
            int finalI = i;
            new Thread(() -> {
                bankAccount.transferMoney(bankAccount);
            },String.valueOf(i)).start();
        }

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println(bankAccount.money);

    }
}
复制代码

AtomicReferenceFieldUpdater类的简单使用代码演示如下:

class MyVar {
    public volatile Boolean isInit = Boolean.FALSE;
    AtomicReferenceFieldUpdater<MyVar,Boolean> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");


    public void init(MyVar myVar) {
        if(atomicReferenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE))
{
            System.out.println(Thread.currentThread().getName()+"\t"+"---init.....");
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+"---init.....over");
        }else{
            System.out.println(Thread.currentThread().getName()+"\t"+"------其它线程正在初始化");
        }
    }


}


/**
 * 多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能初始化一次
 */
public class AtomicIntegerFieldUpdaterDemo
{
    public static void main(String[] args) throws InterruptedException
    {
        MyVar myVar = new MyVar();

        for (int i = 1; i <=5; i++) {
            new Thread(() -> {
                myVar.init(myVar);
            },String.valueOf(i)).start();
        }
    }
}
复制代码

面试官问你:你在哪里用了volatile? AtomicReferenceFieldUpdater

6. 原子操作增强类原理深度解析

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

阿里开发手册中:

  • 【参考】volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。
  • 说明:如果是 count++ 操作,使用如下类实现:Atomiclnteger count=new AtomicInteger();count.addAndget(1)如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)

举例子:

  • 热点商品点赞计算器,点赞数加加统计,不要求实时精确
  • 一个很大的List,里面都是int类型,如何实现加加,说说思路

点赞计数器,看看性能

常用API如下图所示:

方法名 说明
void add(long x) 将当前的value加x
void increment() 将当前的value加1
void decrement() 将当前的value减1
long sum() 返回当前值。特别注意,在没有并发更新value的情况下,sum会返回一个精确值,在存在并发的情况下,sum不保证返回精确值
void reset() 将value重置为0,可用于替代重新new一个LongAdder,但此方法只可以在没有并发更新的情况下使用。
long sumThenReset() 获取当前value,并将value重置为O。

LongAdder 只能用来计算加法,且从零开始计算LongAccumulator提供了自定义的函数操作,代码演示如下:

// long类型的聚合器,需要传入一个long类型的二元操作,可以用来计算各种聚合操作,包括加乘等
public class LongAccumulatorDemo {

    LongAdder longAdder = new LongAdder();
    public void add_LongAdder() {
        longAdder.increment();
    }

    // LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y,0);
    LongAccumulator longAccumulator = new LongAccumulator(new LongBinaryOperator() {
        @Override
        public long applyAsLong(long left, long right) {
            return left - right;
        }
    },777);

    public void add_LongAccumulator() {
        longAccumulator.accumulate(1);
    }

    public static void main(String[] args) {
        LongAccumulatorDemo demo = new LongAccumulatorDemo();

        demo.add_LongAccumulator();
        demo.add_LongAccumulator();
        System.out.println(demo.longAccumulator.longValue());
    }
}
复制代码

LongAdderAPIDemo代码如下:

public class LongAdderAPIDemo
{
    public static void main(String[] args)
    {
        LongAdder longAdder = new LongAdder();

        longAdder.increment();
        longAdder.increment();
        longAdder.increment();

        System.out.println(longAdder.longValue());

        LongAccumulator longAccumulator = new LongAccumulator((x,y) -> x * y,2);

        longAccumulator.accumulate(1);
        longAccumulator.accumulate(2);
        longAccumulator.accumulate(3);

        System.out.println(longAccumulator.longValue());

    }
}
复制代码

LongAdder高性能对比Code演示,代码演示如下:


class ClickNumber {
    int number = 0;
    public synchronized void add_Synchronized() {
        number++;
    }

    AtomicInteger atomicInteger = new AtomicInteger();
    public void add_AtomicInteger() {
        atomicInteger.incrementAndGet();
    }

    AtomicLong atomicLong = new AtomicLong();
    public void add_AtomicLong() {
        atomicLong.incrementAndGet();
    }

    LongAdder longAdder = new LongAdder();
    public void add_LongAdder() {
        longAdder.increment();
        //longAdder.sum();
    }

    LongAccumulator longAccumulator = new LongAccumulator((x,y) -> x+y,0);
    public void add_LongAccumulator() {
        longAccumulator.accumulate(1);
    }

}


/**
 *
 *  50个线程,每个线程100W次,总点赞数出来
 */
public class LongAdderCalcDemo
{
    public static final int SIZE_THREAD = 50;
    public static final int _1W = 10000;

    public static void main(String[] args) throws InterruptedException
    {
        ClickNumber clickNumber = new ClickNumber();
        long startTime;
        long endTime;

        CountDownLatch countDownLatch1 = new CountDownLatch(SIZE_THREAD);
        CountDownLatch countDownLatch2 = new CountDownLatch(SIZE_THREAD);
        CountDownLatch countDownLatch3 = new CountDownLatch(SIZE_THREAD);
        CountDownLatch countDownLatch4 = new CountDownLatch(SIZE_THREAD);
        CountDownLatch countDownLatch5 = new CountDownLatch(SIZE_THREAD);
        //========================

        startTime = System.currentTimeMillis();
        for (int i = 1; i <=SIZE_THREAD; i++) {
            new Thread(() -> {
                try
                {
                    for (int j = 1; j <=100 * _1W; j++) {
                        clickNumber.add_Synchronized();
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    countDownLatch1.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch1.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t add_Synchronized"+"\t"+clickNumber.number);


        startTime = System.currentTimeMillis();
        for (int i = 1; i <=SIZE_THREAD; i++) {
            new Thread(() -> {
                try
                {
                    for (int j = 1; j <=100 * _1W; j++) {
                        clickNumber.add_AtomicInteger();
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    countDownLatch2.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch2.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t add_AtomicInteger"+"\t"+clickNumber.atomicInteger.get());

        startTime = System.currentTimeMillis();
        for (int i = 1; i <=SIZE_THREAD; i++) {
            new Thread(() -> {
                try
                {
                    for (int j = 1; j <=100 * _1W; j++) {
                        clickNumber.add_AtomicLong();
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    countDownLatch3.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch3.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t add_AtomicLong"+"\t"+clickNumber.atomicLong.get());

        startTime = System.currentTimeMillis();
        for (int i = 1; i <=SIZE_THREAD; i++) {
            new Thread(() -> {
                try
                {
                    for (int j = 1; j <=100 * _1W; j++) {
                        clickNumber.add_LongAdder();
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    countDownLatch4.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch4.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t add_LongAdder"+"\t"+clickNumber.longAdder.longValue());

        startTime = System.currentTimeMillis();
        for (int i = 1; i <=SIZE_THREAD; i++) {
            new Thread(() -> {
                try
                {
                    for (int j = 1; j <=100 * _1W; j++) {
                        clickNumber.add_LongAccumulator();
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    countDownLatch5.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch5.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t add_LongAccumulator"+"\t"+clickNumber.longAccumulator.longValue());
    }
}
复制代码

上述代码运行结果如下:

在这里插入图片描述

6.1 源码、原理分析

6.1.1 架构

在这里插入图片描述

6.1.2 LongAdder为什么这么快?

LongAdderStriped64的子类:

public class LongAdder extends Striped64 implements Serializable {

abstract class Striped64 extends Number {
复制代码

原理(LongAdder为什么这么快)?

1. Striped64

Striped64有几个比较重要的成员函数,如下:

/** Number of CPUS, to place bound on table size */
// CPU数量,即cells数组的最大长度
static final int NCPU = Runtime.getRuntime().availableProcessors();

/**
 * Table of cells. When non-null, size is a power of 2.
 */
 // cells数组,为2的幂,方便以后位运算
transient volatile Cell[] cells;

/**
 * Base value, used mainly when there is no contention, but also as
 * a fallback during table initialization races. Updated via CAS.
 */
 // 基础value的值,当并发降低时,只累加该值主要用于没有竞争的情况,通过CAS更新
transient volatile long base;
/**
 * Spinlock (locked via CAS) used when resizing and/or creating Cells.
 */
 // 创建或者扩容cells数组时使用的自旋锁变量调整单元格大小(扩容),创建单元格时使用的锁
transient volatile int cellsBusy;
复制代码

最重要2个如下:

image.png

Striped64中一些变量或者方法的定义,如下:

  • base:类似于AtomicLong中全局的value值。在没有竞争情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
  • collide: 表示扩容意向,false一定不会扩容,true可能会扩容。
  • cellsBusy: 初始化cells或者扩容cells需要获取锁,0:表示无锁状态1:表示其他线程已经持有了锁
  • casCellsBusy():通过CAS操作修改cellsBusy的值,CAS成功代表获取锁,返回true
  • NCPU:当前计算机CPU数量,Cell数组扩容时会使用到
  • getProbe():获取当前线程的hash值
  • advanceProbe():重置当前线程的hash值

2. Cell

Celljava.util.concurrent.atomicStriped64 的一个内部类,

image.png

3. 原理(LongAdder为什么这么快)

LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回

sum()会将所有Cell数组中的valuebase累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点

在这里插入图片描述 在这里插入图片描述

即内部有一个base变量,一个Cell[]数组

  • base变量:非竞态条件下,直接累加到该变量上
  • Cell[]数组竞态条件下,累加各个线程自己的槽Cell[i]中

6.2 源码解读深度分析

LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零的做法,从空间换时间,用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和无竞争值base都加起来作为最终结果。

在这里插入图片描述

6.2.1 longAdder.increment()

/**
* Equivalent to {@code add(1)}.
*/
public void increment() {
   add(1L);
}
复制代码

1. add(1L)

  • as : 表示cells引用
  • b : 表示获取的base值
  • v : 表示期望值
  • m: 表示cells数组的长度
  • a : 表示当前线程命中的cells单元格

条件递增,逐步解析,如下:

    1. 最初无竞争时只更新base;
    1. 如果更新base失败后,首次新建一个Cell[]数组
    1. 当多个线程竞争同一个Cell比较激烈时,可能就要对Cell[]扩容
public void add(long x) {
   - as : 表示`cells引用`
   - b : 表示获取的`base值`
   - v : 表示`期望值`
   - m: 表示`cells数组的长度`
   - a : 表示当前线程`命中的cells单元格`
   Cell[] as; long b, v; int m; Cell a;
   // 首次首线程(as = cells) != null一定是false,此时走casbase方法,以cas的方式更新base值,且只有cas失败时,才会走到if里面
   // 条件1:cells不为空,说明出现过竞争,cell[]已创建
   // 条件2:cas操作base失败,说明其他线程先一步修改了base,正在出现竞争
   if ((as = cells) != null || !casBase(b = base, b + x)) {
       // true无竞争,false竞争激烈,多个线程hash到同一个cell,可能要扩容
       boolean uncontended = true;
       //条件1:cells为空,说明正在出现竞争,上面是从条件2过来的
       //条件2:应该不会出现
       //条件3:当前线程所在的cell为空,说明当前线程还没有更新过cell,应初始化一个cell
       //条件4:更新当前线程所在的cell失败,说明现在竞争很激烈,多个线程hash到同一个cell,应扩容
       if (as == null || (m = as.length - 1) < 0 ||
           // getProbe()方法返回的是线程中的threadLocalRandomProbe字段
           // 它是通过随机生成一个值,对于一个确定的线程这个值是固定的(除非可以修改它)
           (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x)))
           // 调用Striped64中的方法处理
           longAccumulate(x, null, uncontended);
   }
}
复制代码

代码对应的流程图如下:

在这里插入图片描述

2. longAccumulate方法

longAccumulate入参说明如下:

  • long x 需要增加的值,一般默认都是1
  • LongBinaryOperator fn 默认传递的是null
  • wasUncontended竞争标识,如果是false则代表有竟争。只有cells初始化之后,并且当前线程CAS竞争修改失败,才会是false

步骤如下:

  • 线程hash值:probe,如下:

    final void longAccumulate(long x, LongBinaryOperator fn,
                          boolean wasUncontended) {
        // 存储线程probe的值
        int h;
        // 如果getProbe()获取为0,说明随机数没有初始化
        if ((h = getProbe()) == 0) {
            // 使用ThreadLocalRandom 为当前线程重新计算一个hash值,强制初始化
            ThreadLocalRandom.current(); // force initialization
            // 重新获取probe值,hash值被重置就好比一个全新的线程一样,所以设置了wasUncontended竞争状态为true
            h = getProbe();
            // 重新计算了当前线程的hash后认为此次不算是一次竞争,都未初始化,肯定还不存在竞争激烈
            wasUncontended = true;
        }
        ...未完
    }
    
    static final int getProbe() {
        return UNSAFE.getInt(Thread.currentThread(), PROBE);
    }
    
    private static final long PROBE;
    
    
    PROBE = UNSAFE.objectFieldOffset
        (tk.getDeclaredField("threadLocalRandomProbe"));
        
    
    /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;
    复制代码
  • 总的步骤总纲如下:

final void longAccumulate(long x, LongBinaryOperator fn,
                          boolean wasUncontended) {
    // 存储线程probe的值
    int h;
    // 如果getProbe()获取为0,说明随机数没有初始化
    if ((h = getProbe()) == 0) {
        // 使用ThreadLocalRandom 为当前线程重新计算一个hash值,强制初始化
        ThreadLocalRandom.current(); // force initialization
        // 重新获取probe值,hash值被重置就好比一个全新的线程一样,所以设置了wasUncontended竞争状态为true
        h = getProbe();
        // 重新计算了当前线程的hash后认为此次不算是一次竞争,都未初始化,肯定还不存在竞争激烈
        wasUncontended = true;
    }
    boolean collide = false;                // True if last slot nonempty
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        //case1:cell[]数组已经初始化了
        if ((as = cells) != null && (n = as.length) > 0) {
            if ((a = as[(n - 1) & h]) == null) { // 当前线程的hash值运算后映射到cell单元为null,说明cell没有被使用
                if (cellsBusy == 0) {       // cell[]数组没有,正在扩容
                    Cell r = new Cell(x);   // 创建一个cell单元
                    if (cellsBusy == 0 && casCellsBusy()) { //尝试加锁,成功后cellBusy=1
                        boolean created = false;
                        try {               // 在有锁的情况下再检测一遍之前的判断
                            Cell[] rs; int m, j; // 在cell单元赋到Cell[]数组上
                            if ((rs = cells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                rs[j] = r;
                                created = true;
                            }
                        } finally {
                            cellsBusy = 0;
                        }
                        if (created)
                            break;
                        continue;           // Slot is now non-empty
                    }
                }
                collide = false;
            }
            else if (!wasUncontended)  //wasUncontended 表示前一次CAS更新Cell单元是否成功了 
                wasUncontended = true;      //重新置为true,后面会重新计算线程的hash值
            else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                         fn.applyAsLong(v, x)))) //尝试CAS 更新Cell单元值
                break;
            else if (n >= NCPU || cells != as)// 当cell数组的大小超过CPU核数后,永远不会再进行扩容
                collide = false;            // 扩容标识,置为false,表示不会再进行扩容
            else if (!collide)
                collide = true;
            else if (cellsBusy == 0 && casCellsBusy()) { //尝试加锁进行扩容
                try {
                    if (cells == as) {      // Expand table unless stale
                        Cell[] rs = new Cell[n << 1]; //扩容后的大小 = 当前容量*2
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i];
                        cells = rs;
                    }
                } finally {
                    cellsBusy = 0;
                }
                collide = false;
                continue;                   // Retry with expanded table
            }
            h = advanceProbe(h); // 计算当前线程新的hash值
        }
        // case2:cell[] 没有加锁且没有初始化,则尝试对它进行加锁,并初始化cells数组
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false;
            try {                           // Initialize table
                // 不double check,就会再次new一个cell数组,上一个线程对应数组中的值将会被修改
                if (cells == as) {
                    Cell[] rs = new Cell[2]; //新建一个大小为2的Cell数组
                    rs[h & 1] = new Cell(x); //找到当前线程hash到数组中的位置并创建其对应的Cell
                    cells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        // case3:cell[]数组正在初始化中,则尝试直接在基数base上进行累加操作
        else if (casBase(v = base, ((fn == null) ? v + x :
                                    fn.applyAsLong(v, x))))
            break;                          // Fall back on using base
    }
}
复制代码

上述代码首先给当前线程分配一个hash值,然后进入一个for(;;)自旋,这个自旋分为三个分支:

  • CASE1:Cell[]数组已经初始化

  • CASE2:Cell[]数组未初始化(首次新建)

  • CASE3:Cell[]数组正在初始化中

  • 计算

    • 一开始刚刚要初始化Cell[]数组(首次新建),即未初始化过Cell[]数组,尝试占有锁并首次初始化cells数组 在这里插入图片描述

      • 如果上面条件都执行成功就会执行数组的初始化及赋值操作, Cell[] rs = new Cell[2]表示数组的长度为2,rs[h & 1] = new Cell(x) 表示创建一个新的Cell元素,value是x值,默认为1。h & 1类似于我们之前HashMap常用到的计算散列桶index的算法,通常都是hash & (table.len - 1)。同hashmap一个意思
    • 兜底的else模块,即多个线程尝试CAS修改失败的线程会走到这个分支,如下: 在这里插入图片描述

      • 该分支实现直接操作base基数,将值累加到base上,也即其它线程正在初始化,多个线程正在更新base的值。
    • 对于Cell数组不再为空且可能存在Cell数组扩容分析如下: 多个线程同时命中一个cell的竞争,总体代码如下: 在这里插入图片描述

      • 步骤一: 在这里插入图片描述

      • 上面代码判断当前线程hash后指向的数据位置元素是否为空,如果为空则将Cell数据放入数组中,跳出循环。如果不空则继续循环。

      • 步骤二: 在这里插入图片描述

      • 步骤三: 在这里插入图片描述 说明当前线程对应的数组中有了数据,也重置过hash值,这时通过CAS操作尝试对当前数中的value值进行累加x操作,x默认为1,如果CAS成功则直接跳出循环。

      • 步骤四: 在这里插入图片描述

      • 步骤五: 在这里插入图片描述

      • 步骤六: 在这里插入图片描述

      • 上6步骤总结如下: 在这里插入图片描述

3. sum方法

为啥在并发情况下sum的值不精确? sum执行时,并没有限制对base和cells的更新(一句要命的话)。所以LongAdder不是强一致性的,它是最终一致性的

首先,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base已经被更新了,而此时局部变量sum不会更新,造成不一致。其次,这里对cell的读取也无法保证是最后一次写入的值。所以,sum方法在没有并发的情况下,可以获得正确的结果。 代码如下:

public long longValue() {
    return sum();
}

/**
 * Returns the current sum.  The returned value is <em>NOT</em> an
 * atomic snapshot; invocation in the absence of concurrent
 * updates returns an accurate result, but concurrent updates that
 * occur while the sum is being calculated might not be
 * incorporated.
 *
 * @return the sum
 */
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;
}
复制代码

6.3 小总结

  • AtomicLong:

    • 线程安全,可允许一些性能损耗,要求高精度时可使用
    • 保证精度,性能代价
    • AtomicLong 是多个线程针对单个热点值value进行原子操作
  • LongAdder:

    • 当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用
    • 保证性能,精度代价
    • LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作

7. 小总结

  • AtomicLong
    • 原理:CAS+自旋

    • 场景:

      • 低并发下的全局计算
      • AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题。

缺陷: 高并发后性能急剧下降,AtomicLong的自旋会成为瓶颈,N个线程CAS操作修改线程的值,每次只有一个成功过,其它N - 1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,一下子cpu就打高了。

LongAdder 原理:

  • CAS+Base+Cell数组分散
  • 空间换时间并分散了热点数据

场景:高并发下的全局计算

缺陷:sum求和后还有计算线程修改结果的话,最后结果不够准确

参考资料

Java并发编程知识体系
Java并发编程的艺术
Java多线程编程核心技术
Java并发实现原理 JDK源码剖析

猜你喜欢

转载自juejin.im/post/7076632642061336606