050_java.util.concurrent.atomic.Striped64

继承结构

当我们进行并发统计的时候,为了避免线程安全问题,我们会使用AtomicInteger或者AtomicLong进行计数。一般我们会配合while循环一直进行cas直到累加成功。如果并发量很高的情况下,其内部的cas会一直自旋,性能并不高,因此Striped64诞生。Striped64是一个抽象类,支持高性能累加操作。先看看它的继承体系:
image.png
可以看到Striped64继承自Number,证明其是一个数字,其下有四个子类,分别是针对Long/Double的Adder/Accumulator,两两组合产生4个子类。这里先说下这里的高性能底层原理:首先存在一个base,并发不高的场合下将对base进行修改,如果并发高了之后,将会找到其下的Cell数组,根据当前线程进行hash找到对应的Cell进行修改,意味着其逻辑是将并发分拆到不同位置上进行操作。当需要获得总数的时候,将会 统计base与cell数组下的数据进行累计求和达到高性能的目的。

接下来我们先看看Striped64是怎么回事,然后挑选LongAdder与LongAccumulator进行分析。

Striped64

首先看看Striped64内所定义的关键字段:

// 通过分治的思想将对 base 的竞争分散到不同的 cell 单元中。
transient volatile Cell[] cells;

// 无竞争时直接更新 base
// 有竞争时同时更新 base 和 cells 数组
transient volatile long base;

// Spinlock 自旋锁
transient volatile int cellsBusy;

// Cell 对象封装了每个计算单元的值和常用方法。
@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;
            // 获取 value 属性偏移量
            valueOffset = UNSAFE.objectFieldOffset
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

首先存在一个叫做base的东西,没有竞争的时候直接更新base,当有竞争的场合下则更新Cell,可以看到每个Cell内部都存在一个value值,而多个Cell组成了数组。查看Cell的类定义可以发现,存在@sun.misc.Contended注解,这个注解帮助我们解决伪共享问题。所谓伪共享问题也很好理解,a,b位于同一个缓存行的场合下,针对a的更新会导致b同时失效,实际上b不需要失效,导致真正用到b的时候又得加载。如此反复性能自然就弱了。解决办法也很粗暴,就是在a边上挂上一些没什么用的对象,让他们独享一个缓存行。因此效率变高但是缓存占用量也会变多,算是用空间换时间的策略了。

getProbe

在冲突的时候需要定位到使用哪个Cell进行操作,这个时候就需要使用当前的线程求得其hash值

static final int getProbe() {
    return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

 private static final long PROBE;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> sk = Striped64.class;
            BASE = UNSAFE.objectFieldOffset
                (sk.getDeclaredField("base"));
            CELLSBUSY = UNSAFE.objectFieldOffset
                (sk.getDeclaredField("cellsBusy"));
            Class<?> tk = Thread.class;
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

如下代码所示,其值来自于Thread的threadLocalRandomProbe字段。类似在进入longAccumulate方法的的时候,threadLocalRandomProbe一直都是0,当发生争用后才会进入longAccumulate方法中,进入该方法第一件事就是判断threadLocalRandomProbe是否为0,如果为0,则将其设置为0x9e3779b9。probeGenerator 是static 类型的AtomicInteger类,每执行一次localInit()方法,都会将probeGenerator 累加一次0x9e3779b9这个值;,0x9e3779b9这个数字的得来是 2^32 除以一个常数,这个常数就是传说中的黄金比例 1.6180339887;然后将当前线程的threadLocalRandomProbe设置为probeGenerator 的值,如果probeGenerator 为0, 则取1;

final void longAccumulate(long x, LongBinaryOperator fn,
                          boolean wasUncontended) {
    int h;
    // 判断是否为0
    if ((h = getProbe()) == 0) {
        ThreadLocalRandom.current(); // force initialization
        h = getProbe();
        wasUncontended = true;
    }
    ...
}

// ThreadLocalRandom类
private static final int PROBE_INCREMENT = 0x9e3779b9

public static ThreadLocalRandom current() {
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        //初始化
        localInit();
    return instance;
}

static final void localInit() {
    // probeGenerator是个AtomicInteger
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    int probe = (p == 0) ? 1 : p; // skip 0
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    UNSAFE.putLong(t, SEED, seed);
    UNSAFE.putInt(t, PROBE, probe);
}

和probe相关的还有一个,如下是重新生成prod的方法:

static final int advanceProbe(int probe) {
    probe ^= probe << 13;   // xorshift
    probe ^= probe >>> 17;
    probe ^= probe << 5;
    UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
    return probe;
}

longAccumulate方法

类似longAccumulate的方法还有doubleAccumulate,其内部实现差不多,这里仅针对longAccumulate进行分析

final void longAccumulate(long x, LongBinaryOperator fn,
                          boolean wasUncontended) {
    // probe == 0 说明当前线程是第一次进入,则初始化probe作为hash值
    int h;
    if ((h = getProbe()) == 0) {
        //强制初始化
        ThreadLocalRandom.current(); // force initialization
        h = getProbe();
        // 为 true 表示没有竞争 
        wasUncontended = true;
    }
    // cas冲突标志,表示当前线程hash到的Cells数组的位置,做cas累加操作时与其它线程发生了冲突,cas失败;
    // collide=true代表有冲突,collide=false代表无冲突
    boolean collide = false;                // True if last slot nonempty
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        
        //这个主干if有三个分支 :
        //1.主分支一:处理cells数组已经正常初始化了的情况(这个if分支处理add方法的四个条件中的3和4) 
        //2.主分支二:处理cells数组没有初始化或者长度为0的情况;(这个分支处理add方法的四个条件中的1和2) 
        //3.主分支三:处理cells数组没有初始化,并且其它线程正在执行对cells数组初始化的操作,及cellbusy=1, 则尝试将累加值通过cas累加到base上 
        
        //主分支一: 数组已初始化
        if ((as = cells) != null && (n = as.length) > 0) {
            // 内部小分支一:如果被hash到的位置为null,则用x值作为初始值创建一个新的Cell对象,对cells数组使用cellsBusy加锁
            if ((a = as[(n - 1) & h]) == null) {
                //cellsBusy == 0 代表当前没有线程cells数组做修改
                if (cellsBusy == 0) {       // Try to attach new Cell
                    Cell r = new Cell(x);   // Optimistically create
                    //如果cellsBusy=0无锁,则通过cas将cellsBusy设置为1加锁
                    if (cellsBusy == 0 && casCellsBusy()) {
                        boolean created = false;
                        try {               // Recheck under lock
                            Cell[] rs; int m, j;
                            //再次检查cells数组不为null,且长度不为空,且hash到的位置的Cell为null
                            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;
                        //如果created为false,说明上面指定的cells数组的位置cells[m%cells.length]已经有其它线程设置了cell了,继续执行循环。
                        continue;           // Slot is now non-empty
                    }
                }
                
                //如果执行的当前行,代表cellsBusy=1,有线程正在更改cells数组,代表产生了冲突,将collide设置为false
                collide = false;
            }
            // 内部小分支二:通过cas设置cells[m%cells.length]位置的Cell对象中的value值设置为v+x失败,说明已经发生竞争,将
            // wasUncontended设置为true,跳出内部的if判断,最后重新计算一个新的probe,然后重新执行循环;
            else if (!wasUncontended)       // CAS already known to fail
                //设置未竞争标志位true,继续执行,后面会算一个新的probe值,然后重新执行循环。
                wasUncontended = true;      // Continue after rehash
            //内部小分支三:新的争用线程参与争用的情况:处理刚进入当前方法时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;            // At max size or stale
            //内部小分支五:如果发生了冲突collide=false,则设置其为true;会在最后重新计算hash值后,进入下一次for循环
            else if (!collide)
                //设置冲突标志,表示发生了冲突,需要再次生成hash,重试。 如果下次重试任然走到了改分支此时collide=true,!collide条件不成立,则走后一个分支
                collide = true;
            //内部小分支六:扩容cells数组,新参与cell争用的线程两次均失败,且符合扩容条件,会执行该分支
            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
            }
            //为当前线程重新计算hash值
            h = advanceProbe(h);
        }
        //分支二: 数组未初始化
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            //初始化标志
            boolean init = false;
            try {                           // Initialize table
                if (cells == as) {
                    //初始化cells数组,初始容量为2,并将x值通过hash&1,放到0个或第1个位置上
                    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
    }
}

LongAdder

LongAdder有了Striped64之后,其代码就相对比较好理解了,例如我们使用LongAdder进行累加计算的时候:

public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    /**
     * 先判断cells数组有没有初始化,没有初始化则尝试cas对base进行更新
     * 若有线程竞争,则进入下一个if,uncontended是表示无竞争的意思
     */
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        /**
         *  as == null || (m = as.length - 1) < 0 表示如下:
         * 先判断as也就是cells数组有没有进行初始化,如果没有初始化就直接进入longAccumulate方法初始化
         */
        if (as == null || (m = as.length - 1) < 0 ||
            /**
             * a = as[getProbe() & m]) == null表示如下:
             * 1.先根据线程的hash值(getProbe()方法表示获取其线程hash值)& m得到index
             * 2.然后判断此数组下标中是否有元素,如果没有元素就直接进入longAccumulate方法
             */
            (a = as[getProbe() & m]) == null ||
            /**
             * !(uncontended = a.cas(v = a.value, v + x))表示如下:
             * 1.若有元素,则通过CAS将需要加上的值x加到此数组下标上
             * 2.若CAS返回失败则直接进入longAccumulate方法
            */
            !(uncontended = a.cas(v = a.value, v + x)))

            //处理涉及初始化、调整大小、创建新单元格和/或争用的更新情况。
            //这种方法遇到了乐观重试代码常见的非模块化问题,它依赖于重新检查的读集。
            longAccumulate(x, null, uncontended);
    }
}

当我们需要计算总量的时候:

// 遍历cell增加到base上获得总量
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;
}

LongAccumulator

LongAccumulator的实现与LongAdder类似,不同点在于LongAccumulator可以传入LongBinaryOperator进行自定义计算:

public void accumulate(long x) {
    Cell[] as; long b, v, r; int m; Cell a;
    if ((as = cells) != null ||
        (r = function.applyAsLong(b = base, x)) != b && !casBase(b, r)) {
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[getProbe() & m]) == null ||
            !(uncontended =
              (r = function.applyAsLong(v = a.value, x)) == v ||
              a.cas(v, r)))
            longAccumulate(x, function, uncontended);
    }
}

同理,在进行求和的时候,也需要传入的LongBinaryOperator进行协作计算:

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

总结

AtomicInteger可以被用来当做并发场景下的累加器,存在大量并发的时候cas有可能在空转带来性能损耗。这个时候就可以使用Striped64的子类进行处理。其内部存在base与cell数组,并发存在的时候会自动挑选Cell单元进行存储,本质上相当于平摊了并发到各个Cell上。在统计的时候会将base与Cell中的数据进行加和。

  • 24
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的`java.util.concurrent.atomic.AtomicReference`提供了一种线程安全的方式来更新对象引用。它通过使用CAS(Compare-And-Swap)算法实现了原子性的操作。 下面是一个简单的案例,展示如何使用`AtomicReference`类: ```java import java.util.concurrent.atomic.AtomicReference; public class AtomicReferenceExample { public static void main(String[] args) { // 初始化AtomicReference对象 AtomicReference<String> atomicReference = new AtomicReference<>("Hello"); // 获取当前对象引用的值 String currentValue = atomicReference.get(); System.out.println("Current value: " + currentValue); // 比较并替换 boolean updated = atomicReference.compareAndSet("Hello", "World"); System.out.println("Value updated: " + updated); // 获取更新后的值 String updatedValue = atomicReference.get(); System.out.println("Updated value: " + updatedValue); } } ``` 在上面的示例中,我们首先创建了一个`AtomicReference`对象,并初始化为字符串`"Hello"`。然后我们使用`compareAndSet`方法比较当前对象引用的值是否为`"Hello"`,如果是,则将其替换为`"World"`。最后,我们获取更新后的值,并输出到控制台。 需要注意的是,`AtomicReference`类提供了许多其他有用的方法,如`set`、`getAndSet`、`weakCompareAndSet`等,可以根据具体的需求选择使用。 总之,`AtomicReference`类是Java中一种非常有用的线程安全对象引用类,可以避免多个线程同时修改对象引用时出现的竞争条件问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值