LongAdder-Prinzip

Schaffen Sie weiter, beschleunigen Sie das Wachstum! Dies ist der 7. Tag meiner Teilnahme an der „Nuggets Daily New Plan · June Update Challenge“, klicken Sie hier, um die Details der Veranstaltung anzuzeigen

Gestaltungsideen

Es gibt einen internen Variablenwert in AtomicLong, der den tatsächlichen Long-Wert enthält, und alle Operationen werden für diese Variable ausgeführt. Das heißt, in einer Umgebung mit hoher Parallelität ist die Wertvariable tatsächlich ein Hotspot, d. h. N Threads konkurrieren um einen Hotspot. Die Grundidee von LongAdder besteht darin, Hotspots zu verteilen, die Wertwerte in einem Array zu verteilen, verschiedene Threads treffen auf verschiedene Slots im Array, und jeder Thread führt nur CAS-Operationen für den Wert in seinem eigenen Slot aus, sodass die Hotspots sind verstreut und Konflikte sind viel unwahrscheinlicher. Wenn Sie den echten Long-Wert erhalten möchten, akkumulieren Sie einfach die Variablenwerte in jedem Slot und kehren Sie zurück.

Bild.png

Interne Struktur von LongAdder

Es gibt eine Basisvariable in LongAdder, ein Cell[]-Array:

  • Basisvariable: Bei Nicht-Rennbedingungen wird sie direkt zu dieser Variablen akkumuliert
  • Cell[]-Array: Akkumuliert unter Race-Bedingungen den eigenen Slot jedes Threads Cell[i]
/** Number of CPUS, to place bound on table size */ 
// CPU核数,用来决定槽数组的大小 
static final int NCPU = Runtime.getRuntime().availableProcessors(); 

/** 
* Table of cells. When non‐null, size is a power of 2. 
*/ 
// 数组槽,大小为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. 
*/ 
/** 
* 基数,在两种情况下会使用: 
* 1. 没有遇到并发竞争时,直接使用base累加数值18 * 2. 初始化cells数组时,必须要保证cells数组只能被初始化一次(即只有一个线程能对cell s初始化), 
* 其他竞争失败的线程会讲数值累加到base上 
*/ 
transient volatile long base; 

/** 
* Spinlock (locked via CAS) used when resizing and/or creating Cells. 
*/ 
transient volatile int cellsBusy; 
复制代码

Eine interne Cell-Klasse ist definiert, das ist der zuvor erwähnte Slot. Jedes Cell-Objekt hat einen Wert, und sein Wert kann von CAS über Unsafe manipuliert werden:

LongAdder#add-Methode

Die Logik der LongAdder#add-Methode ist wie folgt:

public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        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);
    }
}
复制代码

Die Basiskardinalität wird nur verwendet, wenn es noch nie einen Parallelitätskonflikt gegeben hat Sobald ein Parallelitätskonflikt auftritt, werden alle nachfolgenden Operationen nur an der Einheitszelle im Array Cell[] ausgeführt. Wenn das Cell[]-Array nicht initialisiert ist, wird longAccumelate der übergeordneten Klasse aufgerufen, um Cell[] zu initialisieren. Wenn Cell[] initialisiert wurde, aber der Konflikt in der Cell-Unit auftritt, wird longAccumelate der übergeordneten Klasse aufgerufen auch genannt werden. ] erweitert. Dies ist auch die Subtilität des Designs von LongAdder: Minimieren Sie Hotspot-Konflikte und versuchen Sie, CAS-Operationen bis zum letzten Ausweg zu verzögern.

Striped64#longAccumulate-Methode

Das gesamte Striped64#longAccumulate lautet wie folgt:

final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
        //获取当前线程的threadLocalRandomProbe值作为hash值,如果当前线程的threadLocalRandomProbe为0,说明当前线程是第一次进入该方法,则强制设置线程的threadLocalRandomProbe为ThreadLocalRandom类的成员静态私有变量probeGenerator的值,后面会详细将hash值的生成;
        //另外需要注意,如果threadLocalRandomProbe=0,代表新的线程开始参与cell争用的情况
        //1.当前线程之前还没有参与过cells争用(也许cells数组还没初始化,进到当前方法来就是为了初始化cells数组后争用的),是第一次执行base的cas累加操作失败;
        //2.或者是在执行add方法时,对cells某个位置的Cell的cas操作第一次失败,则将wasUncontended设置为false,那么这里会将其重新置为true;第一次执行操作失败;
       //凡是参与了cell争用操作的线程threadLocalRandomProbe都不为0;
        int h;
        if ((h = getProbe()) == 0) {
            //初始化ThreadLocalRandom;
            ThreadLocalRandom.current(); // force initialization
            //将h设置为0x9e3779b9
            h = getProbe();
            //设置未竞争标记为true
            wasUncontended = true;
        }
        //cas冲突标志,表示当前线程hash到的Cells数组的位置,做cas累加操作时与其它线程发生了冲突,cas失败;collide=true代表有冲突,collide=false代表无冲突 
        boolean collide = false; 
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            //这个主干if有三个分支
            //1.主分支一:处理cells数组已经正常初始化了的情况(这个if分支处理add方法的四个条件中的3和4)
            //2.主分支二:处理cells数组没有初始化或者长度为0的情况;(这个分支处理add方法的四个条件中的1和2)
            //3.主分支三:处理如果cell数组没有初始化,并且其它线程正在执行对cells数组初始化的操作,及cellbusy=1;则尝试将累加值通过cas累加到base上
            //先看主分支一
            if ((as = cells) != null && (n = as.length) > 0) {
                /**
                 *内部小分支一:这个是处理add方法内部if分支的条件3:如果被hash到的位置为null,说明没有线程在这个位置设置过值,没有竞争,可以直接使用,则用x值作为初始值创建一个新的Cell对象,对cells数组使用cellsBusy加锁,然后将这个Cell对象放到cells[m%cells.length]位置上 
                 */
                if ((a = as[(n - 1) & h]) == null) {
                    //cellsBusy == 0 代表当前没有线程cells数组做修改
                    if (cellsBusy == 0) {
                        //将要累加的x值作为初始值创建一个新的Cell对象,
                        Cell r = new Cell(x); 
                        //如果cellsBusy=0无锁,则通过cas将cellsBusy设置为1加锁
                        if (cellsBusy == 0 && casCellsBusy()) {
                            //标记Cell是否创建成功并放入到cells数组被hash的位置上
                            boolean created = false;
                            try {
                                Cell[] rs; int m, j;
                                //再次检查cells数组不为null,且长度不为空,且hash到的位置的Cell为null
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    //将新的cell设置到该位置
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                //去掉锁
                                cellsBusy = 0;
                            }
                            //生成成功,跳出循环
                            if (created)
                                break;
                            //如果created为false,说明上面指定的cells数组的位置cells[m%cells.length]已经有其它线程设置了cell了,继续执行循环。
                            continue;
                        }
                    }
                   //如果执行的当前行,代表cellsBusy=1,有线程正在更改cells数组,代表产生了冲突,将collide设置为false
                    collide = false;
 
                /**
                 *内部小分支二:如果add方法中条件4的通过cas设置cells[m%cells.length]位置的Cell对象中的value值设置为v+x失败,说明已经发生竞争,将wasUncontended设置为true,跳出内部的if判断,最后重新计算一个新的probe,然后重新执行循环;
                 */
                } else if (!wasUncontended)  
                    //设置未竞争标志位true,继续执行,后面会算一个新的probe值,然后重新执行循环。 
                    wasUncontended = true;
                /**
                *内部小分支三:新的争用线程参与争用的情况:处理刚进入当前方法时threadLocalRandomProbe=0的情况,也就是当前线程第一次参与cell争用的cas失败,这里会尝试将x值加到cells[m%cells.length]的value ,如果成功直接退出  
                */
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                /**
                 *内部小分支四:分支3处理新的线程争用执行失败了,这时如果cells数组的长度已经到了最大值(大于等于cup数量),或者是当前cells已经做了扩容,则将collide设置为false,后面重新计算prob的值*/
                else if (n >= NCPU || cells != as)
                    collide = false;
                /**
                 *内部小分支五:如果发生了冲突collide=false,则设置其为true;会在最后重新计算hash值后,进入下一次for循环
                 */
                else if (!collide)
                    //设置冲突标志,表示发生了冲突,需要再次生成hash,重试。 如果下次重试任然走到了改分支此时collide=true,!collide条件不成立,则走后一个分支
                    collide = true;
                /**
                 *内部小分支六:扩容cells数组,新参与cell争用的线程两次均失败,且符合库容条件,会执行该分支
                 */
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        //检查cells是否已经被扩容
                        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
                }
                //为当前线程重新计算hash值
                h = advanceProbe(h);
 
            //这个大的分支处理add方法中的条件1与条件2成立的情况,如果cell表还未初始化或者长度为0,先尝试获取cellsBusy锁。
            }else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {  // Initialize table
                    //初始化cells数组,初始容量为2,并将x值通过hash&1,放到0个或第1个位置上
                    if (cells == as) {
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    //解锁
                    cellsBusy = 0;
                }
                //如果init为true说明初始化成功,跳出循环
                if (init)
                    break;
            }
            /**
             *如果以上操作都失败了,则尝试将值累加到base上;
             */
            else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) // Fall back on using base
                break;  
        }
    }
复制代码

LongAdder#sum-Methode

/**
 * 返回累加的和,也就是"当前时刻"的计数值
 * 注意: 高并发时,除非全局加锁,否则得不到程序运行中某个时刻绝对准确的值
 * 此返回值可能不是绝对准确的,因为调用这个方法时还有其他线程可能正在进行计数累加,
 * 方法的返回时刻和调用时刻不是同一个点,在有并发的情况下,这个值只是近似准确的计数值
 */
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;
}
复制代码

Da das Cell-Array bei der Berechnung der Summe nicht gesperrt ist, können andere Threads den Wert in der Zelle während des Akkumulationsprozesses ändern und das Array auch erweitern, sodass der von sum zurückgegebene Wert nicht sehr genau ist atomarer Momentaufnahmewert des Aufrufs der Summenmethode.

LongAkkumulator

LongAccumulator ist eine erweiterte Version von LongAdder. LongAdder kann nur Additions- und Subtraktionsoperationen für numerische Werte ausführen, während LongAccumulator benutzerdefinierte Funktionsoperationen bereitstellt. Sein Konstruktor sieht wie folgt aus: Durch LongBinaryOperator können Sie jede Operation an den Eingabeparametern anpassen und das Ergebnis zurückgeben (LongBinaryOperator empfängt 2 Longs als Parameter und gibt 1 Long zurück). Das interne Prinzip von LongAccumulator ist fast genau dasselbe wie das von LongAdder, die beide die longAccumulate-Methode der übergeordneten Klasse Striped64 verwenden.

public class LongAccumulatorTest { 
    
    public static void main(String[] args) throws InterruptedException { 
        // 累加 x+y 
        LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0); 
        ExecutorService executor = Executors.newFixedThreadPool(8); 
        // 1到9累加 
        IntStream.range(1, 10).forEach(i -> executor.submit(() -> 
        accumulator.accumulate(i))); 
        Thread.sleep(2000); 
        System.out.println(accumulator.getThenReset()); 
    } 
}
复制代码

Verweise

Ich denke du magst

Origin juejin.im/post/7103872764984950797
Empfohlen
Rangfolge