第九章: 原子操作类

原子类位于java.util.concurrent.atomic 包下

image-20221126195414071

  1. AtomicBoolean

  2. AtomicInteger

  3. AtomicIntegerArray

  4. AtomicIntegerFieldUpdater

  5. AtomicLong

  6. AtomicL ongArray

  7. AtomicL ongFieldUpdater

  8. AtomicMarkableReference

  9. AtomicReference

  10. AtomicReferenceArray

  11. AtomicReferenceFieldUpdater

  12. AtomicStampedReference

  13. DoubleAccumulator

  14. DoubleAdder

  15. LongAccumulator

  16. LongAdder

基本类型原子类

处理的数据类型不同,API 都相同

  • AtomicInteger
  • AtomicBoolean
  • AtomicLong

构造器,以 AtomicInteger 为例,另外俩个也一样

Constructor and Description
AtomicInteger() 创建一个新的AtomicInteger,初始值为 0
AtomicInteger(int initialValue) 用给定的初始值创建一个新的AtomicInteger。

常用的 API

public final int get()获取当前值
public final int getAndSet(int new Value)获取当前值,并设置新值
public final int getAndIncrement()获取当前值,并自增
public final int getAndDecrement()获取当前值,并自减
public final int getAndAdd(int delta)获取当前值,并加上预期的值
public comapreAndSet(int expect,int update)如果内存中的数值等于预期值,则以原子的方式将该值设置为输入值(update)

案例一:

创建 50 个线程,每个线程执行 1000 次自增+1操作,预计结果为 50000

public class AtomicTest01 {
    private static final int SIZE = 50;
    public static void main(String[] args) {
        Number number = new Number();
        for (int i = 1; i <= SIZE; i++) {
            new Thread(() -> {
                for (int i1 = 0; i1 < 1000; i1++) {
                    number.add();
                }
            }, String.valueOf(i
            )).start();
        }
        // 获取最终值
        System.out.println(Thread.currentThread().getName() + "\t" + number.atomicInteger.get());
    }
}

class Number{
    // 默认值为0
    AtomicInteger atomicInteger = new AtomicInteger();

    public void add() {
        // 自增+1
        atomicInteger.getAndIncrement();
    }
}

输出结果:

main	47693

与我们预期的结果并不一样,造成这样的原因是: main主线程执行的太快,其他线程还没有运算完,main线程就获取计算结果。导致与预计结果不一样。

解决方法

  • 可以在主线程中 sleep ,等待其他线程计算完。但是这种方式很傻,并且在工作中如果这样用,那绝对是人人喊打。。。。。
  • 使用 CountDownLatch 计数器
    • 不是很了解的可以去看我博客:JUC基础
public class AtomicTest01 {
    private static final int SIZE = 50;
    public static void main(String[] args) {
        // 创建 50 个线程,每个线程执行 1000 次自增操作
        Number number = new Number();
        // 创建 CountDownLatch 计数器
        CountDownLatch latch = new CountDownLatch(SIZE);
        for (int i = 1; i <= SIZE; i++) {
            new Thread(() -> {
                try {
                    for (int i1 = 0; i1 < 1000; i1++) {
                        number.add();
                    }
                } finally {
                    // 每执行完一个线程就将计数器-1
                    latch.countDown();
                }
            }, String.valueOf(i
            )).start();
        }
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 获取最终值
        System.out.println(Thread.currentThread().getName() + "\t" + number.atomicInteger.get());
    }
}

class Number{
    // 默认值为0
    AtomicInteger atomicInteger = new AtomicInteger();

    public void add() {
        // 自增+1
        atomicInteger.getAndIncrement();
    }
}

数组类型原子类

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicRreferenceArray

构造器,以 AtomicIntegerArray 为例

Constructor and Description
AtomicIntegerArray(int length) 创建给定长度的新 AtomicIntegerArray,所有元素最初为零。
AtomicIntegerArray(int[] array) 创建一个新的 AtomicIntegerArray,其长度与从给定数组复制的所有元素相同。

常用的 API

intget(int i) 获取索引为 i的当前值。
intaddAndGet(int i, int delta) 将索引为 i 的元素加上指定值(delta)
booleancompareAndSet(int i, int expect, int update) 将索引为 i 的元素与预期值(expect)作比较,相等则更新为新值(update)
intget(int i) 获取索引为 i 的元素
intgetAndIncrement(int i) 获取索引为 i 的元素,并加1
intgetAndDecrement(int i) 获取索引为 i 的元素,并减1
public class AtomicArrayTest {
    public static void main(String[] args) {

        // 创建一个长度为5的原子数组,默认值为0
        // AtomicIntegerArray integerArray = new AtomicIntegerArray(5);

        // 指定创建的原子数组
        AtomicIntegerArray integerArray = new AtomicIntegerArray(new int[]{1,2,3,4});

        // 将索引为0的值加2
        int num = integerArray.addAndGet(0, 2);
        System.out.println(num);

        // 将索引为0的元素与3作比较,相等更新值为5
        boolean result = integerArray.compareAndSet(0, 3, 5);
        System.out.println(result);

        // 获取索引为3的元素
        int num_3 = integerArray.get(3);
        System.out.println(num_3);

        // 将索引为 1 的元素增1
        int andIncrement = integerArray.getAndIncrement(1);
        System.out.println(andIncrement);

        System.out.println(integerArray.toString());
    }
}

引用类型原子类

  • AtomicReference
  • AtomicStampedReference
  • AtomicMarkableReference

AtomicReference:在上一章已经讲过,它可以将我们指定的类型纳入原子类中。

AtomicStampedReference:带版本戳以防CAS中的ABA问题(上一章讲过),可以解决多次修改

AtomicMarkableReference : 类似 AtomicStampedReference,使用标记记录内存值是否被修改,但是它使用的是 true、false 标记。只能使用一次,只能判断是否修改过。并不能记录修改几次。

案例演示

/**
 *
 * Author: YZG
 * Date: 2022/11/26 21:04
 * Description: 
 */
public class AtomicMarkableReferenceDemo {
    public static void main(String[] args) {
        AtomicMarkableReference<Object> markableReference = new AtomicMarkableReference<>(100,false);
        new Thread(()->{
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"\t"+"默认标识"+marked);
            //暂停1秒钟线程,等待后面的T2线程和我拿到一样的模式flag标识,都是false
            try {
                TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
            markableReference.compareAndSet(100, 1000, marked, !marked);
        },"t1").start();

        new Thread(()->{
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"\t"+"默认标识"+marked);
            //这里停2秒,让t1先修改,然后t2试着修改
            try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
            boolean t2Result = markableReference.compareAndSet(100, 1000, marked, !marked);
            System.out.println(Thread.currentThread().getName()+"\t"+"t2线程result--"+t2Result);
            System.out.println(Thread.currentThread().getName()+"\t"+markableReference.isMarked());
            System.out.println(Thread.currentThread().getName()+"\t"+markableReference.getReference());

        },"t2").start();
    }
}

输出结果

t1	默认标识false
t2	默认标识false
t2	t2线程result--false
t2	true
t2	1000

Process finished with exit code 0

对象的属性修改原子类

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

更加细粒度范围内的原子更新

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

比如:我们一个银行账户的类,希望只对money字段保证其原子性,其中银行名称,银行账号和姓名都属于公开信息。以前我们使用 synchronized 关键字锁住了整个对象,这种方式锁的范围过大,效率低。

因此我们可以使用 FieldUpdater 原子类进行操作,只保证对象中的某个字段的原子性。

image-20221126211236212

使用要求

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

AtomicIntegerFieldUpdater 案例演示

创建10个线程,每个线程转账 1000,不使用 synchronized

public class AtomicIntegerFieldUpdaterTest {
    public static void main(String[] args) throws InterruptedException {
        Account account = new Account();
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    for (int i1 = 0; i1 < 1000; i1++) {
                        account.addByFieldUpdate();
                    }
                } finally {
                    latch.countDown();
                }
            }, String.valueOf(i)).start();
        }
        latch.await();

        System.out.println(Thread.currentThread().getName()+ "\t" + account.money);
    }
}

class Account{
    String bankName = "CCB";
    // 更新的对象属性必须使用==public volatile==修饰符
    public volatile int money ;

    public  void add() {
        money++;
    }

    // 因为对象的属性修改类型原子类都是**抽象类**,所以每次使用都必须使用静态方法`newUpdater()`创建一个更新器,并且需要设置想要更新的类和属性。
    AtomicIntegerFieldUpdater<Account> integerFieldUpdater =
            AtomicIntegerFieldUpdater.newUpdater(Account.class,"money");

    public void addByFieldUpdate() {
        integerFieldUpdater.getAndIncrement(this);
    }
}

AtomicReferenceFieldUpdater 案例演示

AtomicReferenceFieldUpdater 可以为指定字段类型保证原子性

//比如这个案例中是针对boolean类型的
class MyVar{
    public volatile Boolean isInit = Boolean.FALSE;
    AtomicReferenceFieldUpdater<MyVar,Boolean> referenceFieldUpdater =
            AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");
    public void init(MyVar myVar){
        if(referenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE)){
            System.out.println(Thread.currentThread().getName()+"\t"+"-----start init,needs 3 seconds");
            try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
            System.out.println(Thread.currentThread().getName()+"\t"+"-----over init");
        }else{
            System.out.println(Thread.currentThread().getName()+"\t"+"抱歉,已经有其他线程进行了初始化");
        }
    }
}

public class AtomicReferenceFieldUpdaterDemo {
    public static void main(String[] args) {
        MyVar myVar = new MyVar();
        for(int i = 1;i <= 5;i ++){
            new Thread(()->{
                myVar.init(myVar);
            },String.valueOf(i)).start();
        }
    }
}
//1  -----start init,needs 3 seconds
//5  抱歉,已经有其他线程进行了初始化
//4  抱歉,已经有其他线程进行了初始化
//2  抱歉,已经有其他线程进行了初始化
//3  抱歉,已经有其他线程进行了初始化
//1  -----over init

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

  • DoubleAccumulator、
  • DoubleAdder
  • LongAccumulator
  • LongAdder

这四个类都是 jdk8 新增的。下面以LongAdder和LongAccumulator进行演示,DoubleAdder和 DoubleAccumulator 仅仅是操作类型不同

LongAdder

jdk 中的描述:

这个类是通常优选AtomicLong当多个线程更新时使用,用于诸如收集统计信息,不用于细粒度同步控制的共同总和。在低更新争议下,这两类具有相似的特征。但是,在高度争议的情况下,这一类的预期吞吐量明显高于牺牲更高的空间消耗

在低并发下,LongAdder 和 AtomicLong 类似,但是在高并发下 LongAdder 比 后者具有更高的吞吐量、性能

构造器

Constructor and Description
LongAdder() 创建一个新的加法器,初始和为零。

常用 API

image-20221127195540693

代码演示

public class LongAdderTest {
    public static void main(String[] args) {
        
        LongAdder longAdder = new LongAdder();
        
        longAdder.increment(); // +1
        longAdder.add(2); // +2
        longAdder.decrement(); // -1 

        System.out.println(longAdder.sum()); // 2
    }
}

LongAccumulator

jdk中这样描述

当多线程更新用于收集统计信息的公共值时,此类通常优于AtomicLong,而不是细粒度的同步控制。 在低更新争议下,这两类具有相似的特征。 但是,在高度争议的情况下,这一类的预期吞吐量明显高于牺牲更高的空间消耗。

构造器

Constructor and Description
LongAccumulator(LongBinaryOperator accumulatorFunction, long identity) 使用给定的累加器函数和identity元素创建一个新的实例。

常用API

Modifier and TypeMethod and Description
voidaccumulate(long x) 具有给定值的更新。
longget() 返回当前值。

LongAdder和 LongAccumulator 的区别

  • LongAdder 只能从默认值 0 开始计算,并且只能进行加法运算。
  • LongAccumulator 支持自定义函数计算。并且可以指定初始值。

代码演示

        // 初始值为1,计算函数为:x+y,初始值相当于 y,accumulate传入的值为x 
        LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 1);
        longAccumulator.accumulate(2); // +2 ,相当于参数 x

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

演示高并发下 LongAdder和 LongAccumulator 的性能

前面我们说过,在高并发下,LongAdder和 LongAccumulator 的性能优于 AtomicLong。

下面就实现一个点赞功能,演示: synchronized 、Atomic、LongAdder和 LongAccumulator 的性能对比。

代码演示

/**
 *
 * Author: YZG
 * Date: 2022/11/27 20:26
 * Description: 
 */
public class ClickDemo {
    public static void main(String[] args) throws InterruptedException {
        Resource resource = new Resource();
        CountDownLatch latch1 = new CountDownLatch(50);
        CountDownLatch latch2 = new CountDownLatch(50);
        CountDownLatch latch3 = new CountDownLatch(50);
        CountDownLatch latch4 = new CountDownLatch(50);
        long begin;
        long end;

        begin = System.currentTimeMillis();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                try {
                    for (int i1 = 0; i1 < 1000000; i1++) {
                        resource.clickBySynchronized();
                    }
                } finally {
                    latch1.countDown();
                }
            }, String.valueOf(i)).start();
        }
        latch1.await();
         end = System.currentTimeMillis();
        System.out.println("clickBySynchronized 耗时: " + (end - begin) + "毫秒" + " , 点赞结果: " + resource.number);

        // ==========================================================================================================
        begin = System.currentTimeMillis();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                try {
                    for (int i1 = 0; i1 < 1000000; i1++) {
                        resource.clickByAtomicLong();
                    }
                } finally {
                    latch2.countDown();
                }
            }, String.valueOf(i)).start();
        }
        latch2.await();
        end = System.currentTimeMillis();
        System.out.println("clickByAtomicLong 耗时: " + (end - begin) + "毫秒" + " , 点赞结果: " + resource.number);

        // ==========================================================================================================
        begin = System.currentTimeMillis();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                try {
                    for (int i1 = 0; i1 < 1000000; i1++) {
                        resource.clickByLongAdder();
                    }
                } finally {
                    latch3.countDown();
                }
            }, String.valueOf(i)).start();
        }
        latch3.await();
        end = System.currentTimeMillis();
        System.out.println("clickByLongAdder 耗时: " + (end - begin) + "毫秒" + " , 点赞结果: " + resource.number);

        // ==========================================================================================================
        begin = System.currentTimeMillis();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                try {
                    for (int i1 = 0; i1 < 1000000; i1++) {
                        resource.clickByLongAccumulator();
                    }
                } finally {
                    latch4.countDown();
                }
            }, String.valueOf(i)).start();
        }
        latch4.await();
        end = System.currentTimeMillis();
        System.out.println("clickByLongAccumulator 耗时: " + (end - begin) + "毫秒" + " , 点赞结果: " + resource.number);
    }
}
// 资源类
class Resource {
    // 点赞数
    public long number ;

    // synchronized方式
    public synchronized void clickBySynchronized() {
        number++;
    }

    // AtomicLong方式
    AtomicLong atomicLong = new AtomicLong(0);
    public void clickByAtomicLong() {
        atomicLong.incrementAndGet();
    }

    // LongAdder方式
    LongAdder longAdder =  new LongAdder();
    public void clickByLongAdder() {
        longAdder.increment();
    }

结果:

clickBySynchronized 耗时: 2958毫秒 , 点赞结果: 50000000
clickByAtomicLong 耗时: 1335毫秒 , 点赞结果: 50000000
clickByLongAdder 耗时: 219毫秒 , 点赞结果: 50000000
clickByLongAccumulator 耗时: 158毫秒 , 点赞结果: 50000000

通过测试可以看出,LongAdder 和 LongAccumulato消耗的时间比Synchronized降低了将近十几倍,因此也可以说明在高并发下 LongAdder 和 LongAccumulato性能确实优秀!

LongAdder 原理分析

从阿里和官网上就已经说明了 LongAdder 性能之高,并推荐在高并发下使用。

阿里手册

image-20221127210726829

官网

image-20221127210559673

那么LongAdder 为什么会这么快呢?底层又是什么样呢?一起研究一下。

LongAdder 的继承关系图

首先看 LongAdder 的继承关系:

image-20221127210444928

从继承关系图中得知,LongAdder 继承了 Striped64 ,Striped64 继承了 Number类,我们就先从 Striped64 这个类入手

Striped64

类中有着四个属性,这四个属性的含义如下:

其中最重要的是 cells 和 base ,后面我们会用到。


    /** 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的幂,2,4,8,16.....,方便以后位运算
    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;

类中的方法和变量

image-20221127211342548

Cell 类 是 Striped64 的一个静态内部类


    /**
     * Padded variant of AtomicLong supporting only raw accesses plus CAS.
     *
     * JVM intrinsics note: It would be possible to use a release-only
     * form of CAS here, if it were provided.
     */
    @sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long valueOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

LongAdder 为什么这么快?

在高并发下LongAdder替换掉 AtomicLong,为什么要替换呢?

AtomicLong 底层采用的 CAS,靠硬件实现原子性和可见性,保证在内存中只有一个线程能够获取到数据,那么其他线程就必须在主内存外进行自旋, 这种方式在低并发的情况下还可以忍受,我们可以想想,如果在高并发线程下,几千个 ,几万个线程同时在自旋,那么这种情况对于 CPU 的消耗是非常严重的。性能会急剧下降

image-20221127213928945

LongAdder 可以理解为 AtomicLong 的升级版,那么他是如何解决 CPU 的空转呢?

LongAdder 在低并发情况下其实和 AtomicLong一样,使用 base 记录 value值,但是在高并发下就不一样,在高并发下,LongAdder 采用分散热点 的方式,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。

举个例子:火车站买火车票,AtomicLong 只要一个窗口,其他人都在排队;而LongAdder 利用cell开了多个卖票窗口,所以效率高了很多。

image-20221127215722550

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

image-20221127220220080

image-20221127220308412

LongAdder 源码分析

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

在源码中,LongAdder 是如何计算 hash 值 并 映射到 cell 数组下标的呢?

add 方法

首先,先看 LongAdder 中的 自增 方法:

    public void increment() {
        add(1L);
    }

调用了 add 方法


    /**
     * Adds the given value.
     *
     * @param x the value to add
     */
    public void add(long x) {
        // as 表示 cells 引用
        // b 表示获取的base值
        // v 表示期望值
        // m 表示 cells 数组的长度
        // a 表示当前线程命中的  cell 单元格
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            // uncontended代表没有冲突。
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }

casBase 方法里调用的就是 UNSAFE 类中的 compareAndSwapLong ,这个方法大家都熟悉,就是比较并交换,将期望值与内存中的值作比较,相等则交换新值。

因此 casBase 这个方法就是 判断 base 和内存中的值,如果相等就交换 b+x 。

    final boolean casBase(long cmp, long val) {
        //  this 表示要操作的对象,BASE 内存地址偏移量,cmp 期望值  val 交换的值
        return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
    }

当一个线程调用 add 方法,(as = cells) != null = false 。base初始值为0,执行 casBase方法,会将 b+x 与 内存中的值交换,此时内存中的值为1,返回 true,取反等于 false,因此 跳出 if 语句 结束。

image-20221129172513898

因此在线程少的情况下,会一直操作 base 值,并不会采用 cells 数组分散热点

但是当高并发时,在执行 !casBase(b = base, b + x) 这段代码时,肯定会有某个线程的 base 值与内存中的值不一样,交换失败,返回 false。就会执行 if 语句里面的代码。

as == null = true 会执行 longAccumulate 方法, longAccumulate 方法里就是 as 数组进行初始化,也就是将 base 的值分散到 cells 数组中:

image-20221129173147887

我们可以看到在 longAccumulate方法里,将 cells 数组进行初始化,容量为 2 ,也就是此时 base、cells[0]、cells[1] 都可以供线程获取值。

当有线程再执行 add() 方法时,其中 (as = cells) != null = true 又会跳出 if 语句,结束执行。

image-20221129173257745

因此,在低并发下,只需要执行 casBase 方法进行比较交换就行,不会涉及到cells 分散热点。

但是在高并发下会执行 longAccumulate 方法, 对 cells 数组进行初始化或者扩容。将base的值分散到 cells 数组中供其他线程访问。

总结一下 add方法

图像

longAccumulate方法

上面中说道 longAccumulate 方法对 cells 数组进行扩容和初始化,那么在该方法中具体怎么做呢?

清楚 longAccumulate 方法的参数

image-20221129174005128

longAccumulate 方法在Striped64类中,看一下Striped64类中需要用的方法以及属性

image-20221129174359832

首先看 longAccumulate 的第一部分:获取线程的 probe 值

调用 getProbe() 方法获取当前线程的 探测值(hash值),探测值你可以理解为某个工厂中工人的工号 , 每个线程对应一个值,通过这个值才会找到对应 cells 数组的槽位。

如果在一些特殊情况下,获取不到 (h = getProbe()) == 0 = true , 会强制获取一个随机值。并且将此线程的竞争状态设置为 true, 表示不参与竞争。

image-20221129174733855


    /**
     * Returns the probe value for the current thread.
     * Duplicated from ThreadLocalRandom because of packaging restrictions.
     * 获取当前线程的探测值
     */
    static final int getProbe() {
        return UNSAFE.getInt(Thread.currentThread(), PROBE);
    }

更详细的说明

image-20221129174753427

longAccumulate 的第二部分: 是一个 大的 for 自旋,for 循环里面又分为三部分。

image-20221129175758827

在第一部分中 首先给当前线程分配一个hash值,然后进入一个for(;;)自旋,这个自旋分为三个分支:

  • CASE1:Cell[]数组已经初始化
  • CASE2:Cell[]数组未初始化(首次新建)
  • CASE3:Cell[]数组正在初始化中

首先,在 base 转换使用 cells 数组来分散热点时,需要初始化 cells 数组,也就是 CASE2,第二部分:

先看一下整体的思路

image-20221129182146841

            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }

cellsBusy 是锁的状态,0 表示没有线程占用,1 表示有线程占用。

在初始化cells 数组的时候只需要有一个线程来初始化,因此在某个线程进来后 会先拿到锁,并且调用 casCellsBusy 方法将锁的状态修改成 1

    final boolean casCellsBusy() {
        // CAS: CELLSBUSY 默认值为0,
        return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
    }

获取锁后,就是初始化 cells 的步骤,创建一个大小容量为 2 的 Cell 数组。

                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;

在初始化完毕之后,执行 finally 语句块释放锁。


当某个线程在初始化的时候,其他线程怎么办呢?因此就需要了解第三步部分 CASE3 了。第三部分就是其他线程在等待某一个线程初始化要执行的。
在这里插入图片描述

在第三部分中,其他等待的线程会直接操作 base进行 CAS。

           // 在上面得知 
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base

当 初始化 Cells 数组完毕之后,就执行 第一部分 CASE1了,将 CASE1 分为6小部分,分析:

if ((as = cells) != null && (n = as.length) > 0) {
				// 当线程的 hash 值运算后映射得到的 Cell单元为null,说明该Cell 没有被使用
                if ((a = as[(n - 1) & h]) == null) {
                // Cell[] 数组没有正在扩容
                    if (cellsBusy == 0) {       // Try to attach new Cell
                    	// 创建一个 Cell 单元
                        Cell r = new Cell(x);   // Optimistically create
                        // 尝试加锁,成功后 casCellsBusy = 1
                        if (cellsBusy == 0 && casCellsBusy()) {
                            boolean created = false;
                            // 在有锁的情况下,在检查一下之前的判断
                            try {               // Recheck under lock
                                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;
                }
                /*
                  CASE1.1  上面代码判断当前线程hash后指向的数据位置元素是否为空,
                            如果为空则将Cell数据放入数组中,跳出循环。
                            如果不空则继续循环
                */                
                

                else if (!wasUncontended)       
                    wasUncontended = true;    
                /*
                  CASE1.2  wasUncontended表示cells初始化后,当前线程竞争修改失败
                            wasUncontended =false,这里只是重新设置了这个值为true,紧
                            接着执行advanceProbe(h)重置当前线程的hash,重新循环。

                */    

                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                 /*
                 	CASE1.3  说明当前线程对应的数组中有了数据,也重置过hash值,
                            这时通过CAS操作尝试对当前数中的value值进行累加x操作,x默认为1,如果CAS								成功则直接跳出循环。
                 */   

    
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
    
                /*
                  CASE1.4  如果n大于CPU最大数量,不可扩容,
                            并通过下面的h = advanceProbe(h)方法修
                            改线程的probe再重新尝试
                */
                else if (!collide)
                    collide = true;
                /*
               CASE1.5 如果扩容意向callide是false则修改它为true,然后重新计算当前线
                        程的hash值继续循环,
                        如果当前数组的长度已经大于了CPU的核数,就会再次设置扩容意
                        向collide=false (见上一 步)
                */
    
    			// CASE1.6: 
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        if (cells == as) {      
                            // 按位左移一位来操作,扩容之前的俩倍
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                // 扩容后将之前的数组拷贝到新的数组中
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        // 释放锁,将状态改为0
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                h = advanceProbe(h);
            }

对CASE1 的总结

image-20221129184542698

sum 方法

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

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

为啥在并发情况下sum的值不精确?

sum执行时,并没有限制对base和cells的更新(一句要命的话)。所以LongAdder不是强一致性的,它是最终一致性的

首先,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base已经被更新了,而此时局部变量sum不会更新,造成不一致。

其次,这里对cell的读取也无法保证是最后一次写入的值。所以,sum方法在没有并发的情况下,可以获得正确的结果。

使用总结

AtomicLong

特点

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

原理:

CAS+自旋

应用场景:

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

缺陷 :

高并发后性能急剧下降

N个线程CAS操作修改线程的值,每次只有一个成功过,其它N - 1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,一下子cpu就打高了。

LongAdder

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

原理

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

应用场景:

高并发下的全局计算

缺陷:

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



各位彭于晏,如有收获点个赞不过分吧…✌✌✌

Alt


gongzhonghao 回复 [JUC] 获取MarkDown笔记

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

鲨瓜2号

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

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

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

打赏作者

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

抵扣说明:

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

余额充值