7.原子操作类之18罗汉增强

1、是什么

7.原子操作类之18罗汉增强_第1张图片

7.原子操作类之18罗汉增强_第2张图片


2、基本类型原子类

2.1、AtomicInteger

2.2、AtomicBoolean

2.2、AtomicLong

2.4、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)//比较并交换

2.5、Case

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

    public static void main(String[] args) throws InterruptedException {
        MyNumber myNumber = new MyNumber();
        CountDownLatch countDownLatch = new CountDownLatch(SIZE);
        for (int i = 0; i < SIZE; i++) {
            new Thread(() -> {
                try {

                    for (int j = 0; j < 1000; j++) {
                        myNumber.addPlusPlus();
                    }
                } finally {
                    countDownLatch.countDown();
                }
            }, String.valueOf(i)).start();
        }
        //等待上面50个线程全部计算完成后,再去获得最终值
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName() + "\t" + myNumber.atomicInteger.get());
    }
}

class MyNumber {
    AtomicInteger atomicInteger = new AtomicInteger();

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

3、数组类型原子类

3.1、AtomicIntegerArray

3.2、AtomicLongArray

3.3、AtomicReferenceArray

3.4、Case

7.原子操作类之18罗汉增强_第3张图片

4、引用类型原子类

4.1、AtomicReference

自旋锁SpinLockDemo

可查看 6.8.1、手写

4.2、AtomicStampedReference

携带版本号的引用类型原子类,可以解决ABA问题

解决修改过多少次

可查看 6.9.2.2.1

4.3、AtomicMarkableReference

原子更新带有标记位的引用类型对象

解决是否修改过

它的定义就是将状态戳简化为 true l false

类似一次性筷子


public class AtomicMarkableReferenceDemo {
    static AtomicMarkableReference markableReference = new AtomicMarkableReference(100, false);

    public static void main(String[] args) {
        new Thread(() -> {
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName() + "\t默认标识" + marked);
            //等待后面的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);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = markableReference.compareAndSet(100, 2000, marked, !marked);
            System.out.println(Thread.currentThread().getName() + "\t" + "t2线程CASresult:" + b);
            System.out.println(Thread.currentThread().getName() + "\t" + markableReference.isMarked());
            System.out.println(Thread.currentThread().getName() + "\t" + markableReference.getReference());

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

7.原子操作类之18罗汉增强_第4张图片

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

5.1、AtomicIntegerFieldUpdater

原子更新对象中int类型字段的值

基于反射的实用程序,可对指定类的指定volatile int字段进行原子更新

5.2、AtomicLongFieldUpdater

原子更新对象中Long类型字段的值

5.3、AtomicReferenceFieldUpdater

原子更新对象引用类型字段的值

5.4、使用目的

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

7.原子操作类之18罗汉增强_第5张图片

5.5、使用要求

更新的对象属性必须使用public volatile修饰符。

因为对象的属性修改类型原子类都是抽象类,

  • 所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。

5.6、你在哪使用过volatile?

上述三者都可以

5.7、Case


AtomicIntegerFieldUpdater

/**
 * 以一种线程安全的方式操作非线程安全对象的某些字段。
 * 需求:
 * 10个线程,
 * 每个线程转账1000,
 * 不使用synchronized,尝试使用AtomicIntegerFieldupdater来实现。
 */
public class AtomicIntegerFieldUpdaterDemo {
    public static void main(String[] args) throws InterruptedException {
        BankAccount bankAccount = new BankAccount();
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 1000; j++) {
//                        bankAccount.add();
                        bankAccount.transMoney(bankAccount);
                    }

                } finally {
                    countDownLatch.countDown();

                }
            }).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName() + "\t" + bankAccount.money);


    }
}

class BankAccount {
    String bankName = "CCB";

    //更新对象属性必须使用 public volatile 修饰符
    volatile int money = 0;

    public void add() {
        money++;
    }

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

    AtomicIntegerFieldUpdater<BankAccount> fieldUpdater =
            AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, "money");
    //不加synchronized
    public void transMoney(BankAccount bankAccount){
        fieldUpdater.getAndIncrement(bankAccount);
    }
}

AtomicReferenceFieldUpdater

/**
 * 需求:
 * 多线程并发调用一个类的初始化方法,I如果未被初始化过,将执行初始化工作,
 * 要求只能被初始化一次,只有一个线程操作成功
 */
public class AtomicReferenceFieldUpdaterDemo {
    public static void main(String[] args) {
        MyVar myVar = new MyVar();
        for (int i = 0; i < 9; i++) {
            new Thread(()->{
                myVar.init(myVar);
            },String.valueOf(i)).start();
        }
    }
}

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");
            System.out.println(Thread.currentThread().getName()+"\t----over init");
        }else {
            System.out.println(Thread.currentThread().getName()+"\t -----已经有现成再进行初始化工作");
        }
    }

}

7.原子操作类之18罗汉增强_第6张图片

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

当多个线程更新用于收集统计信息但不用于细拉度同步控制的目的的公共和时,此类通常优于AtomicLong

在低更新争用下,这两个类具有相似的特征。但在高争用的情况下,这一类的预期吐量明显更高,但代价是空间消耗更高。

6.1、DoubleAccumulator

.6.2、DoubleAdder

6.3、LongAccumulator

6.4、LongAdder

6.5、面试题

7.原子操作类之18罗汉增强_第7张图片

6.6、点赞计数器

6.6.1、常用API

7.原子操作类之18罗汉增强_第8张图片

6.6.2、入门讲解

LongAdder只能用来计算加法。且从零开始计算

LongAccumulator提供了自定义的函数操作

Demo

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

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

        System.out.println(longAdder.sum());//3

        LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y,0);
        longAccumulator.accumulate(1);//1 x=0 y=1 x+y=1
        longAccumulator.accumulate(3);//4 x=1 y=3 x+y=4

        System.out.println(longAccumulator.get()); // 4
    }
}

6.6.3、LongAdder高性能对比Code演示

package com.atomics;

/**
 * @PACKAGE_NAME: com.atomics
 * @NAME: AccumulatorCompareDemo
 * @USER: Mrs.Wang
 * @DATE: 2022/9/25
 * @TIME: 17:23
 * @DAY_NAME_SHORT: 周日
 * @DAY_NAME_FULL: 星期日
 * @PROJECT_NAME: JUC
 **/

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

/**
 * 50个线程,每个线程100w从,总点赞出来
 */
public class AccumulatorCompareDemo {
    public static final int _1W = 10000;
    public static final int threadNumber = 50;
    public static void main(String[] args) throws InterruptedException {
        ClickNumber clickNumber = new ClickNumber();
        long startTime;
        long endTime;
        CountDownLatch countDownLatch = new CountDownLatch(threadNumber);
        startTime = System.currentTimeMillis();
        for (int i = 0; i <= threadNumber; i++) {
            new Thread(()->{
                try {
                    for (int j = 0; j < 100 * _1W; j++) {
                        clickNumber.clickByLongAccumulator();
                    }
                }finally {
                    countDownLatch.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime:" + (endTime - startTime) + "ms");
    }
}

class ClickNumber {
    int number = 0;

    //----costTime clickBySynchronized :2217ms
    public synchronized void clickBySynchronized() {
        number++;
    }

    AtomicLong atomicLong = new AtomicLong(0);

    //----costTime: clickByAtomicLong : 1163ms
    public void clickByAtomicLong() {
        atomicLong.getAndIncrement();
    }

    LongAdder longAdder = new LongAdder();

    //----costTime: clickByLongAdder : 138ms
    public void clickByLongAdder() {
        longAdder.increment();
    }

    LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);

    //----costTime: clickByLongAccumulator : 140ms
    public voiad clickByLongAccumulator(){
        longAccumulator.accumulate(1);
    }
}

6.7、源码、原理分析

6.7.1、架构

7.原子操作类之18罗汉增强_第9张图片

6.7.2、LongAdder是Striped64的子类

6.7.3、Striped64

7.原子操作类之18罗汉增强_第10张图片


7.原子操作类之18罗汉增强_第11张图片


7.原子操作类之18罗汉增强_第12张图片

6.7.4、Cell

Striped64的一个内部类

7.原子操作类之18罗汉增强_第13张图片

6.7.5、LongAddr为什么这么快

7.原子操作类之18罗汉增强_第14张图片

一句话

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

sum()会将所有Cell数组中的valuebase累加作为返回值,

核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。

7.原子操作类之18罗汉增强_第15张图片

7.原子操作类之18罗汉增强_第16张图片

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

base变量:

  • 低并发,直接累加到该变量上

Cell[]数组:

  • 高并发,累加进各个线程自己的槽cell[i]中

image-20220927223844509

6.7.6、源码解读深度分析

6.7.6.1、小总结

LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零分散热点的做法,用空间换时间,

​ 用一个数组cells将一个value拆分进这个数组cells

​ 多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cell的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和base都加起来作为最终结果。

7.原子操作类之18罗汉增强_第17张图片

image-20220927224408331

6.7.6.2、longAddr.increment()
6.7.6.2.1、add(1L)

7.原子操作类之18罗汉增强_第18张图片

  1. 最初无竞争时只更新base
  2. 如果更新base失败后,首次新建一个Cell[]数组
  3. 当多个线程竞争同一个Cell比较激烈时,可能要对Cell[]数组扩容
public void add(long x) {
    /**
    	as是striped64中的cells数组属性
		b是Striped64中的base属性
		v是当前线程hash到的Cell中存储的值
		m是cells的长度减1, hash时作为掩码使用
		a是当前线程hash到的cell
    */
    Cell[] as; long b, v; int m; Cell a;
    //首次首线程((as = cells) != null)一定是false,此时走casBase方法,以CAS的方式更base值,且只有当cas失败时,才会走到if中
    //条件1: cells不为空
    //条件2:cas操作base失败,说明其他线程先一步修改了base正在出现竞争
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        //true 无竞争  false表示竞争激烈,多个线程hash到同一个Cell,可能要扩容
        boolean uncontended = true;
        //条件1:cells为空
        //条件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)))
            longAccumulate(x, null, uncontended);
    }
}
  1. 如果cells为空,尝试用CAS更新base字段,成功则退出;

  2. 如果Cells为空CAS更新base字段失败,出现竞争(线程多),uncontendedtrue,调用longAccumulate;

  3. 如果Cells非空,但当前线程映射的槽为空uncontendedtrue,调用longAccumulate;

  4. 如果Cells非空,且前线程映射的槽非空CAS更新Cell的值,成功则返回,否则(线程更多), uncontended设为false,调用longAccumulate

6.7.6.2.2、longAccumulate

longAccumulate入参说明

7.原子操作类之18罗汉增强_第19张图片


7.原子操作类之18罗汉增强_第20张图片

步骤

  • 线程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
        wasUncontended = true;
    }
    ...
}

7.原子操作类之18罗汉增强_第21张图片

7.原子操作类之18罗汉增强_第22张图片


  • 总纲

7.原子操作类之18罗汉增强_第23张图片

image-20220928231405481

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

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

CASE3: Cell[]数组正在初始化中 2 尝试直接在base上累加操作,成功返回。否则重置hash循环


  • 计算

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

未初始化过Cell[]数组,尝试占有锁并首次初始化cells数组

7.原子操作类之18罗汉增强_第24张图片

如果上面条件都执行成功就会执行数组的初始化及赋值操作,Cell[] rs = new Cel[2]表示数组的长度为2rs[h&1]=new Cell(x)表示创建一个新的Cell元素,valuex值,默认为1

h&1类似于我们之前HashMap常用到的计算散列桶index的算法,通常都是hash &(table.len-1)。同hashmap一个意思。

对于dc

假如有两个线程同时走到这销断里面,第一个线程判断cellsBusy == o && cells == as (as为空,cells还没初始化也为空)都是true,此时线程一时间片用完。

线程二进来,它他们判断cellsBusy == o && cells == as 都为true。并且casCellBusy()成功,此时进入if时成功的代玛块。并且执行完毕把cellsBusy变为了0,

此时cells已经初始化,线程二时间片用完。线程一接着执行casCellBusy()并且成功。它也进if成功的代码块。此时try中的if(cellls==as)就为false,防止了多次初始化


兜底

多个线程尝试CAS修改失败的线程会走到这个分支

7.原子操作类之18罗汉增强_第25张图片

该分支实现直接操作base基数,将值累加到base上,也即其它线程正在初始化,多个线程正在更新base的值。


Cell数组不再为空且可能存在Cell数组扩容

多个线程同时命中一个cell的竞争

1、对初始化的Cell数组进行赋值操作

//CASE1:cells已经被初始化了
if ((as = cells) != null && (n = as.length) > 0) {
    if ((a = as[(n - 1) & h]) == null) {//当前线程的hash运算后映射的Cell单元为null,说明Cell没有被使用
        if (cellsBusy == 0) {       // Try to attach new Cell  ,Cell[]数组没有正在扩容,就是锁
            Cell r = new Cell(x);   // Optimistically create  创建一个Cell单元
            if (cellsBusy == 0 && casCellsBusy()) {//尝试加锁,成功后cellsBusy==1,双重检查,防止正在扩容
                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;
    }

上面代码判断当前线程hash后指向的数据位置元素是否为空,

如果为空则将Cell数据放入数组中,跳出循环。

如果不空则继续循环。

else if (!wasUncontended)       // CAS already known to fail
    wasUncontended = true;      // Continue after rehash
...
h = advanceProbe(h);

wasUncontended表示cells初始化后,当前线程竞争修改失败wasUncontended = false

这里只是重新设置了这个值为true

紧接着执行advanceProbe(h)重置当前线程的hash(找其他的槽位),重新循环

+else if (a.cas(v = a.value, ((fn == null) ? v + x :
                             fn.applyAsLong(v, x))))
    break;
...
h = advanceProbe(h);

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

失败的话,h = advanceProbe(h); 重置hash值,找其他的槽位

else if (n >= NCPU || cells != as)
    collide = false;            // At max size or stale
...
h = advanceProbe(h);

如果n大于CPU最大数量,不可扩容,
并通过下面的h = advanceProbe(h)方法修改线程的probe再重新尝试

重置哈希

else if (!collide)
    collide = true;
...
h = advanceProbe(h);

如果扩容意向collidefalse则修改它为true,然后重新计算当前线程的hash值继续循环,
如果当前数组的长度已经大于了CPU的核数,就会再次设置扩容意向collide=false(见上一步)

扩容Cell数组,并且

7.原子操作类之18罗汉增强_第26张图片

总结

7.原子操作类之18罗汉增强_第27张图片

image-20220929000250942

如果槽位为空,cas失败,线程压力大,进入longAccumlate方法,进行对Cell数组初始化操作(双重检查进行初始化),

如果其他线程进来发现正在初始化,尝试直接对base cas + 1操作,如果成功返回,失败重新计算hash值,找其他的槽位循环。

此时Cell初始化两个数组成功了,继续循环Cell不为空 Cell槽位为空,进行赋值操作,其中也是双重检查加锁进行

赋值。成功了直接跳出循环返回。失败了的话重新循环。

如果竞争压力更大了,可能要进行扩容,首先竞争标志位为false,改为true。重新计算hash值,找其他槽位进行循环操作,

如果当前槽位cas +1成功,直接跳出循环返回。失败的话如果是当前Cell进行扩容操作 Cell数组为2的次幂 ,左移一位,否则循环。

6.7.6.2.3、sum()

sum()会将所有Cell数组中的valuebase累加作为返回值。
核心的思想就是将之前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执行时,并没有限制对basecells的更新(一句要命的话)。

所以LongAdder不是强一致性的,它是最终一致性的。

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

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

6.7.7、使用总结

6.7.7.1、AtomicLong(精度高,性能代价)

线程安全,可允许一些性能损耗,要求高精度时可使用

保证精度,性能代价

AtomicLong是多个线程针对单个热点值value进行原子操作

6.7.7.2、LongAddr(性能高,精度代价)

当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用

保证性能,精度代价

LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作

6.8、小总结

6.8.1、AtomicLong

6.8.1.1、原理

CAS+自旋

incrementAndGet

.8.1.2、场景

低并发下的全局计算

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

6.8.1.3、缺陷

高并发后性能急剧下降why?

AtomicLong的自旋会成为瓶颈

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

6.8.2、LongAddr

6.8.2.1、原理

CAS+Base+Cell数组分散

空间换时间并分散了热点数据

6.8.2.2、场景

高并发下的全局计算

6.8.2.3、缺陷

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

你可能感兴趣的:(JUC,java,c++,算法)