【Java 并发】【八】【Atomic】【四】Strimped64分段锁实现原理

发布时间 2023-04-04 15:13:05作者: 酷酷-

1  前言

上一节我们对LongAdder的底层源码、实现机制进行了深入了剖析,包括AtomicInteger在高并发竞争下导致的大量自旋的问题,以及LongAdder是怎么使用分段锁优化这个问题的。我们最后看到longAccumulate托底的方法,这一节我们来深入的分析一下Striped64的分段锁机制的实现。

2  Striped64分段锁底层实现原理

我们上一节看到LongAdder继承自Striped64,底层分段锁还是基于Striped64dlongAccumulate方法,我们直接接着上一节的内容,继续查看longAccumulate的方法源码:

我们先从整体来看下这个方法内部做了几件事:

longAccumulate里面做了几件事情,先从大角度分析一下:
(1)进入方法,黄色部分,首先就是获取一下用户id,如果为0,先强制初始化一下用户id
(2)然后就是进入for死循环里面,只有用户的请求成功处理了,才会退出循环
(3)然后就是下面比较重要的三个大的分支条件

进入备用窗口处理
// cells是备用窗口列表,如果备用窗口不等于null,
// 并且是length>0 说明备用窗口开启了
// 则用户进入这个条件,去备用窗口列表里面处理
if ((as = cells) != null && (n = as.length) > 0)

初始化备用窗口

如果上面的分支不满足,说明 cells == null 或者 cells.length <= 0 说明备用窗口没开启啊,这个时候就要开启一下备用窗口,即进行备用窗口列表数组的初始化工作:

// cellsBusy 是备用窗口列表是否在初始化或者扩容标志
// cellsBusy == 0 说明尚未有人初始化或者扩容
// 这时候执行到这里的线程,就要进行对备用窗口初始化了
// 比如说线程1走到这里,发现没初始化,就要执行一下casCellBusy告诉别人正在初始化
else if (cellsBusy == 0 && cells == as && casCellsBusy())

竞争常规窗口

如果cellsBusy == 1 说明有人在初始化备用窗口或者对备用窗口列表进行扩容,这个时候备用窗口列表不可用,只能去常规窗口争抢了

// 直接对常规窗口争抢,如果成功就退出死循环了
else if (casBase(v = base, ((fn == null) ? v + x :
                            fn.applyAsLong(v, x))))
    break;

我们画个图再加强理解一下:

(1)比如用户1进入了这里之后,首先判断分支1,发现备用窗口没有启用;然后直接进入分支2,去初始化备用窗口
(2)然后用户2也进来了,发现备用窗口没启用,同时发现有人在初始化备用窗口,直接进入分支3,去争抢常规窗口了
(3)备用窗口初始化好了,开启了之后;用户的请求进来都走到分支1,去备用窗口列表去竞争去了。

2.1  初始化备用窗口

由于备用窗口没有开启,所以最开始有一个线程会进入分支2,我们就先来看看分支2的源码实现:

else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
    // init是否初始化完成,最开始设置为false
    boolean init = false;
    try { // Initialize table
        // 然后这里就没什么复杂的,就是创建一个数组
        // 但是这个时候数组里面没有元素,每个元素都是空的
        if (cells == as) {
            Cell[] rs = new Cell[2];
            // 创建一个新的窗口,把自己的操作数x交给新创建的窗口
            rs[h & 1] = new Cell(x);
            cells = rs;
            // 然后设置初始化完成表示为true
            init = true;
        }
    } finally {
        // 重新设置cellsBusy标识,操作完成,没人在初始化或者扩容了
        cellsBusy = 0;
    }
    // 如果初始化完成,退出循环
    if (init)
        break;
}

如果cells备用窗口时null,则初始化一下;同时如果自己被派遣到的窗口格子是空的,则创建一个窗口格子(相当于叫一个工作人员过来)

2.2  备用窗口已开启,则去备用窗口

if ((as = cells) != null && (n = as.length) > 0) {
    // 如果自己被派到的那个备用窗口为null,说明窗口还没人工作,则叫一个工作人员过来负责
    if ((a = as[(n - 1) & h]) == null) {
        // 如果没人在初始化,下面则创建一个窗口(叫一个工作人员过来处理)
        if (cellsBusy == 0) {
            // 创建一个新的窗口,同时提交自己的请求x
            Cell r = new Cell(x);
            // 如果没人在初始化
            if (cellsBusy == 0 && casCellsBusy()) {
                boolean created = false;
                try {
                    Cell[] rs; int m, j;
                    // 再次检查备用窗口列表初始化好了
                    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;
    }
    // 如果到这里已经知道cas操作失败了,则重新设置一下失败标识,进入下一循环重试
    else if (!wasUncontended) // CAS already known to fail
        wasUncontended = true; // Continue after rehash
    // 根据 用户id % 备用窗口总数 算法,自己应该争抢哪个备用窗口
    // 如果争抢成功,则操作结束了,如果失败进入下一循环重试
    else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                 fn.applyAsLong(v, x))))
        break;
    else if (n >= NCPU || cells != as)
        collide = false;
    else if (!collide)
        collide = true;
    // 走到这里,说明上面的操作都失败了,备用窗口竞争还是很激烈
    // 所以需要扩容了,多增加一些备用窗口,缓解竞争激烈的情况
    else if (cellsBusy == 0 && casCellsBusy()) {
        try {
            // 这里就是具体的扩容操作了
            if (cells == as) { // Expand table unless stale
                // 新的备用窗口是原来的2倍
                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
    }
    // 在这里重新计算一下自己的用户id,
    // 使得用户id尽量的分散到不同的窗口,减少竞争
    h = advanceProbe(h);
}

我们在对上面的核心步骤,截图给再说一下:

3  小结

好了Striped64分段锁的源码我们就看到这里,采用了分治法的思想,一个常规窗口+起始的两个备用窗口,随着竞争的变化可能会开辟多个备用窗口再,增长因子为原来的2倍,有理解不对的地方欢迎指正哈。