【Java 并发】【八】【Atomic】【四】Strimped64分段锁实现原理
1 前言
上一节我们对LongAdder的底层源码、实现机制进行了深入了剖析,包括AtomicInteger在高并发竞争下导致的大量自旋的问题,以及LongAdder是怎么使用分段锁优化这个问题的。我们最后看到longAccumulate托底的方法,这一节我们来深入的分析一下Striped64的分段锁机制的实现。
2 Striped64分段锁底层实现原理
我们上一节看到LongAdder继承自Striped64,底层分段锁还是基于Striped64d的longAccumulate方法,我们直接接着上一节的内容,继续查看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倍,有理解不对的地方欢迎指正哈。