Exchanger 相关整理
1. 简介
- Exchanger(交换者)是自 JDK 1.5 起开始提供的工具套件,源于 java.util.concurrent 包。
- 是一个用于线程间协作的工具类。
- Exchanger 用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。
- 此类提供对外的操作是同步的。
- 用于 成对 出现的线程之间交换数据。
- 可以视作双向的同步队列。
- 可应用于基因算法、流水线设计等场景。
2. Exchanger 的原理
- Exchanger 用于进行线程间的数据交换。
- 它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。
- 两个线程通过
exchange()
方法交换数据, 如果第一个线程先执行exchange()
方法,会一直等待第二个线程也执行exchange()
,当两个线程都到达同步点时,两个线程交换数据,将本线程生产出来的数据传递给对方。 - 使用 Exchanger 的重点是成对的线程使用
exchange()
方法。
- 这个类提供一个无参构造函数,两个重载的范型
exchange()
方法。
public V exchange(V x) throws InterruptedException public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
- 在 Exchanger 中,如果一个线程已经到达了
exchanger()
时,对于其伙伴结点的情况分为三种。- 如果伙伴结点在该线程到达之前已经调用了
exchanger()
方法,则唤醒该伙伴结点然后进行数据交换,得到各自数据返回。 - 如果伙伴结点还没有到达交换点,则该线程被挂起,等待伙伴结点到达后被唤醒,完成数据交换。
- 如果当前线程被中断了则抛出异常,或者等待超时,则抛出超时异常。
- 如果伙伴结点在该线程到达之前已经调用了
- Exchanger 有单槽位和多槽位之分,单个槽位在同一时刻只能用于两个线程交换数据,这样在竞争比较激烈的时候,会影响到性能,多个槽位就是多个线程可以同时进行两个的数据交换,彼此之间不受影响,这样可以很好的提高吞吐量。
数据结构
@sun.misc.Contended static final class Node { int index; // arena的下标,多个槽位的时候利用 int bound; // 上一次记录的Exchanger.bound; int collides; // 在当前bound下CAS失败的次数; int hash; // 用于自旋; Object item; // 这个线程的当前项,也就是需要交换的数据; volatile Object match; // 交换的数据 volatile Thread parked; // 线程 } /** * Value representing null arguments/returns from public * methods. Needed because the API originally didn't disallow null * arguments, which it should have. * 如果交换的数据为 null,则用NULL_ITEM 代替 */ private static final Object NULL_ITEM = new Object();
- Node 定义中,index,bound,collides 用于多槽位。
- item 是当前线程需要交换的数据。
- match 是和其它线程交换后的数据,初始为 null。
- parked 是记录线程,用于阻塞和唤醒线程。
2.1 单槽 Exchanger
- Node 是每个线程自身用于数据交换的结点,每个 Node 就代表了每个线程,为了保证线程安全,把线程的 Node 结点放在 ThreadLocal。
- slot 为单槽。
/** The number of CPUs, for sizing and spin control */ private static final int NCPU = Runtime.getRuntime().availableProcessors(); /** * The bound for spins while waiting for a match. The actual * number of iterations will on average be about twice this value * due to randomization. Note: Spinning is disabled when NCPU==1. */ private static final int SPINS = 1 << 10; // 自旋次数 /** * Slot used until contention detected. */ private volatile Node slot; // 用于交换数据的槽位 /** * Per-thread state 每个线程的数据,ThreadLocal 子类 */ private final Participant participant; /** The corresponding thread local class */ static final class Participant extends ThreadLocal<Node> { // 初始值返回Node public Node initialValue() { return new Node(); } }
exchange 方法
- 等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。
没有设定超时时间的 exchange 方法
public V exchange(V x) throws InterruptedException { Object v; Object item = (x == null) ? NULL_ITEM : x; // 转换成空对象 // arena == null, 路由到slotExchange(单槽交换), 如果arena != null或者单槽交换失败,且线程没有被中断,则路由到arenaExchange(多槽交换),返回null,则抛出中断异常 if ((arena != null || (v = slotExchange(item, false, 0L)) == null) && ((Thread.interrupted() || (v = arenaExchange(item, false, 0L)) == null))) throw new InterruptedException(); return (v == NULL_ITEM) ? null : (V) v; }
- arena 为多槽位,如果为 null,则执行
slotExchange()
单槽方法,否则判断线程是否中断,如果中断值抛出 InterruptedException 异常,没有中断则执行arenaExchange()
多槽方法,如果该方法返回 null,抛出中断异常,最后返回结果。
具有超时功能的 exchange 方法
public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException { Object v; Object item = (x == null) ? NULL_ITEM : x;// 转换成空对象 long ns = unit.toNanos(timeout); // arena == null, 路由到slotExchange(单槽交换), 如果arena != null或者单槽交换失败,且线程没有被中断,则路由到arenaExchange(多槽交换),返回null,则抛出中断异常 if ((arena != null || (v = slotExchange(item, true, ns)) == null) && ((Thread.interrupted() || (v = arenaExchange(item, true, ns)) == null))) throw new InterruptedException(); if (v == TIMED_OUT)// 超时 throw new TimeoutException(); return (v == NULL_ITEM) ? null : (V) v; }
- 增加超时的判断。
slotExchange 方法
private final Object slotExchange(Object item, boolean timed, long ns) { Node p = participant.get(); // 获取当前线程携带的Node Thread t = Thread.currentThread(); // 当前线程 if (t.isInterrupted()) // 保留中断状态,以便调用者可以重新检查,Thread.interrupted() 会清除中断状态标记 return null; for (Node q;;) { if ((q = slot) != null) { // slot不为null, 说明已经有线程在这里等待了 if (U.compareAndSwapObject(this, SLOT, q, null)) { // 将slot重新设置为null, CAS操作 Object v = q.item; // 取出等待线程携带的数据 q.match = item; // 将当前线程的携带的数据交给等待线程 Thread w = q.parked; // 可能存在的等待线程(可能中断,不等了) if (w != null) U.unpark(w); // 唤醒等待线程 return v; // 返回结果,交易成功 } // CPU的个数多于1个,并且bound为0时创建 arena,并将bound设置为SEQ大小 if (NCPU > 1 && bound == 0 && U.compareAndSwapInt(this, BOUND, 0, SEQ)) arena = new Node[(FULL + 2) << ASHIFT]; // 根据CPU的个数估计Node的数量 } else if (arena != null) return null; // 如果slot为null, 但arena不为null, 则转而路由到arenaExchange方法 else { // 最后一种情况,说明当前线程先到,则占用此slot p.item = item; // 将携带的数据卸下,等待别的线程来交易 if (U.compareAndSwapObject(this, SLOT, null, p)) // 将slot的设为当前线程携带的Node break; // 成功则跳出循环 p.item = null; // 失败,将数据清除,继续循环 } } // 当前线程等待被释放, spin -> yield -> block/cancel int h = p.hash; // 伪随机,用于自旋 long end = timed ? System.nanoTime() + ns : 0L; // 如果timed为true,等待超时的时间点; 0表示没有设置超时 int spins = (NCPU > 1) ? SPINS : 1; // 自旋次数 Object v; while ((v = p.match) == null) { // 一直循环,直到有线程来交易 if (spins > 0) { // 自旋,直至spins不大于0 h ^= h << 1; // 伪随机算法, 目的是等h小于0(随机的) h ^= h >>> 3; h ^= h << 10; if (h == 0) // 初始值 h = SPINS | (int) t.getId(); else if (h < 0 && (--spins & ((SPINS >>> 1) - 1)) == 0) Thread.yield(); // 等到h < 0, 而spins的低9位也为0(防止spins过大,CPU空转过久),让出CPU时间片,每一次等待有两次让出CPU的时机(SPINS >>> 1) } else if (slot != p) // 别的线程已经到来,正在准备数据,自旋等待一会儿,马上就好 spins = SPINS; // 如果线程没被中断,且arena还没被创建,并且没有超时 else if (!t.isInterrupted() && arena == null && (!timed || (ns = end - System.nanoTime()) > 0L)) { U.putObject(t, BLOCKER, this); // 设置当前线程将阻塞在当前对象上 p.parked = t; // 挂在此结点上的阻塞着的线程 if (slot == p) U.park(false, ns); // 阻塞, 等着被唤醒或中断 p.parked = null; // 醒来后,解除与结点的联系 U.putObject(t, BLOCKER, null); // 解除阻塞对象 } else if (U.compareAndSwapObject(this, SLOT, p, null)) { // 超时或其它(取消),给其它线程腾出slot v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null; break; } } // 归位 U.putOrderedObject(p, MATCH, null); p.item = null; p.hash = h; return v; }
- 执行流程。
- 检查 slot 是否为空(null),不为空,说明已经有线程在此等待,尝试占领该槽位,如果占领成功,与等待线程交换数据,并唤醒等待线程,交易结束,返回。
- 如果占领槽位失败,创建 arena,继续步骤 1 尝试抢占 slot,直至 slot 为空,或者抢占成功,交易结束返回。
- 如果 slot 为空,则判断 arena 是否为空,如果 arena 不为空,返回 null,重新路由到 arenaExchange 方法。
- 如果 arena 为空,说明当前线程是先到达的,尝试占有 slot,如果成功,将 slot 标记为自己占用,跳出循环,继续步骤 5,如果失败,则继续步骤 1。
- 当前线程等待被释放,等待的顺序是先自旋(spin),不成功则让出 CPU 时间片(yield),最后还不行就阻塞(block),spin -> yield -> block。
- 如果超时(设置超时的话)或被中断,则退出循环。
- 最后,重置数据,下次重用,返回结果,结束。
2.2 多槽 Exchanger
- 一个 Node 数组 arena,代表了很多的槽位
private static final int ASHIFT = 7; // 两个有效槽(slot -> Node)之间的字节地址长度(内存地址,以字节为单位),1 << 7至少为缓存行的大小,防止伪共享 private static final int MMASK = 0xff; // 场地(一排槽,arena -> Node[])的可支持的最大索引,可分配的大小为 MMASK + 1 private static final int SEQ = MMASK + 1; // bound的递增单元,确立其唯一性 private static final int NCPU = Runtime.getRuntime().availableProcessors(); // CPU的个数,用于场地大小和自旋控制 static final int FULL = (NCPU >= (MMASK << 1)) ? MMASK : NCPU >>> 1; // 最大的arena索引 private static final int SPINS = 1 << 10; // 自旋次数,NCPU = 1时,禁用 private static final Object NULL_ITEM = new Object();// 空对象,对应null private static final Object TIMED_OUT = new Object();// 超时对象,对应timeout // 多个线程交换/多槽位 private volatile Node[] arena;
arenaExchange 方法
private final Object arenaExchange(Object item, boolean timed, long ns) { Node[] a = arena; // 交换场地,一排slot Node p = participant.get(); // 获取当前线程携带的Node p.index 初始值为 0 for (int i = p.index;;) { // arena的索引,数组下标 int b, m, c; long j; // 原数组偏移量,包括填充值 // 从场地中选出偏移地址为(i << ASHIFT) + ABASE的内存值,也即真正可用的Node //如果i为0,j相当于是 "第一个"槽位 Node q = (Node) U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE); if (q != null && U.compareAndSwapObject(a, j, q, null)) { // 此槽位不为null, 说明已经有线程在这里等了,重新将其设置为null, CAS操作 Object v = q.item; // 取出等待线程携带的数据 q.match = item; // 将当前线程携带的数据交给等待线程 Thread w = q.parked; // 可能存在的等待线程 if (w != null) U.unpark(w); // 唤醒等待线程 return v; // 返回结果, 交易成功 } else if (i <= (m = (b = bound) & MMASK) && q == null) { // 有效交换位置,且槽位为空 p.item = item; // 将携带的数据卸下,等待别的线程来交易 if (U.compareAndSwapObject(a, j, null, p)) { // 槽位占领成功 long end = (timed && m == 0) ? System.nanoTime() + ns : 0L; // 计算出超时结束时间点 Thread t = Thread.currentThread(); // 当前线程 for (int h = p.hash, spins = SPINS;;) { // 一直循环,直到有别的线程来交易,或超时,或中断 Object v = p.match; // 检查是否有别的线程来交换数据 if (v != null) { // 有则返回 U.putOrderedObject(p, MATCH, null); // match重置,等着下次使用 p.item = null; // 清空,下次接着使用 p.hash = h; return v; // 返回结果,交易结束 } else if (spins > 0) { // 自旋 h ^= h << 1; h ^= h >>> 3; h ^= h << 10; // 移位加异或,伪随机 if (h == 0) // 初始值 h = SPINS | (int) t.getId(); else if (h < 0 && // SPINS >>> 1, 一半的概率 (--spins & ((SPINS >>> 1) - 1)) == 0) Thread.yield(); // 每一次等待有两次让出CPU的时机 } else if (U.getObjectVolatile(a, j) != p) spins = SPINS; // 别的线程已经到来,正在准备数据,自旋等待一会儿,马上就好 else if (!t.isInterrupted() && m == 0 && (!timed || (ns = end - System.nanoTime()) > 0L)) { U.putObject(t, BLOCKER, this); // 设置当前线程将阻塞在当前对象上 p.parked = t; // 挂在此结点上的阻塞着的线程 if (U.getObjectVolatile(a, j) == p) U.park(false, ns); // 阻塞, 等着被唤醒或中断 p.parked = null; // 醒来后,解除与结点的联系 U.putObject(t, BLOCKER, null); // 解除阻塞对象 } else if (U.getObjectVolatile(a, j) == p && U.compareAndSwapObject(a, j, p, null)) { if (m != 0) // 尝试缩减 U.compareAndSwapInt(this, BOUND, b, b + SEQ - 1); // 更新bound, 高位递增,低位 -1 p.item = null; // 重置 p.hash = h; i = p.index >>>= 1; // 索引减半,为的是快速找到汇合点(最左侧) if (Thread.interrupted())// 保留中断状态,以便调用者可以重新检查,Thread.interrupted() 会清除中断状态标记 return null; if (timed && m == 0 && ns <= 0L) // 超时 return TIMED_OUT; break; // 重新开始 } } } else p.item = null; // 重置 } else { if (p.bound != b) { // 别的线程更改了bound,重置collides为0, i的情况如下:当i != m, 或者m = 0时,i = m; 否则,i = m-1; 从右往左遍历 p.bound = b; p.collides = 0; i = (i != m || m == 0) ? m : m - 1; // index 左移 } else if ((c = p.collides) < m || m == FULL || !U.compareAndSwapInt(this, BOUND, b, b + SEQ + 1)) { // 更新bound, 高位递增,低位 +1 p.collides = c + 1; i = (i == 0) ? m : i - 1; // 左移,遍历槽位,m == FULL时,i == 0(最左侧),重置i = m, 重新从右往左循环遍历 } else i = m + 1; // 槽位增长 p.index = i; } } }
- 执行流程。
- 从场地中选出偏移地址为(i << ASHIFT)+ ABASE 的内存值,也即第 i 个真正可用的 Node,判断其槽位是否为空,为空,进入步骤 2。
- 不为空,说明有线程在此等待,尝试抢占该槽位,抢占成功,交换数据,并唤醒等待线程,返回,结束。
- 没有抢占成功,进入步骤 9。
- 检查索引是否越界,越界,进入步骤 9。没有越界,进入步骤 3。
- 尝试占有该槽位,抢占失败,进入步骤 1。抢占成功,进入步骤 4。
- 检查 match,是否有线程来交换数据,如果有,交换数据,结束。如果没有,进入步骤 5。
- 检查 spin 是否大于 0,如果不大于 0,进入步骤 6。
- 如果大于 0,检查 hash 是否小于 0,并且 spin 减半或为 0,如果不是,进入步骤 4。
- 如果是,让出 CPU 时间,过一会儿,进入步骤 4。
- 检查是否中断,m 达到最小值,是否超时,如果没有中断,没有超时,并且 m 达到最小值,阻塞,过一会儿进入步骤 4。否则,进入步骤 7。
- 没有线程交换数据,尝试丢弃原有的槽位重新开始,丢弃失败,进入步骤 4。否则,进入步骤 8。
- bound 减 1(m>0),索引减半。
- 检查是否中断或超时,如果没有,进入步骤 1。
- 否则,返回,结束。
- 检查 bound 是否发生变化,如果变化,重置 collides,索引重置为 m 或左移,转向步骤 1。否则,进入步骤 10。
- 检查 collides 是否达到最大值,如果没有,进入步骤13。否则,进入步骤 11。
- m 是否达到 FULL,是,进入步骤13。否则,进入步骤 12。
- CAS bound 加 1 是否成功,如果成功,i 置为 m+1,槽位增长,进入步骤 1。否则,进入步骤 13。
- collides 加 1,索引左移,进入步骤 1。
- 从场地中选出偏移地址为(i << ASHIFT)+ ABASE 的内存值,也即第 i 个真正可用的 Node,判断其槽位是否为空,为空,进入步骤 2。
static final class Participant extends ThreadLocal<Node> {
public Node initialValue() { return new Node(); }
}
- 通过 participant 取得当前结点 Node,然后根据当前结点 Node 的 index 去取 arena 中相对应的结点。
伪随机
h ^= h << 1;
h ^= h >>> 3;
h ^= h << 10;
- xorshift 算法。
- T = (I + La)(I + Rb)(I + Lc)。
- L 代表左移。
- R 代表右移。
- a,b,c 分别为代码中的 1,3,10。
- I 代表矩阵 {0, 1} 共 32 位,即是 int 类型的二进制。
- T 代表的是随机算法。
- T = (I + La)(I + Rb)(I + Lc)。
- 伪随机通过 xorshift 算法模拟随机,为了达到更好的随机效果,周期自然是越大越好。
- 周期 指的是,当给定一个输入,得到的输出再作为下一次的输入,如此反复,直到某次输出恰巧等于最初的输入,这便是随机算法的一个周期。
- int 类型的最大周期应该是遍历该类型所有的值(0 除外(奇异矩阵),如果是 0 ,输出便一直是 0,不能随机),即
max(2^31-1) - min(-2^31) = 2^32 - 1 = 4294967295
。
为什么选用 1,3,10
- 当 a,b,c 分别为 1,3,10 时,周期刚好是
2^32 - 1 = 4294967295
。 - 以下几种组合也是可以的。
[4294967295] (1, 3, 10) (2, 7, 7) (2, 7, 9) (5, 9, 7) (7, 1, 9) (7, 7, 2) (7, 9, 5)
为什么要有两次左移和一次右移
- 虽然只一次左移+异或就能达到随机的效果。
- 但是第一次左移(I + La)可以让高位多 1,右移(I + Rb)可以让低位多 1,高位低位都参与计算,可以增加随机性,第二次左移(I + Lc),再进行真正的随机计算。
自旋等待
private static final int SPINS = 1 << 10;
else if (h < 0 && (--spins & ((SPINS >>> 1) - 1)) == 0) // h < 0,一半的概率
Thread.yield(); // 每一次等待有两次让出CPU的时机
- 等待其它线程交换数据时,会进行自旋等待,自旋的过程中,当前线程会有 2 次让出 CPU 的时机。
- SPINS 为 1024,
((1024 >>>1) -1) = 511 = 0111111111
,spins 默认为 1024 循环递减。 - 当 spins 的最高位为 0 或 1 并且其它位为 0 时(0 或 512)进行 与(&) 计算的结果为 0。
- SPINS 为 1024,
arena 的创建
static final int FULL = (NCPU >= (MMASK << 1)) ? MMASK : NCPU >>> 1;
private static final int ASHIFT = 7;
private static final int NCPU = Runtime.getRuntime().availableProcessors();
private static final int MMASK = 0xff; // 255
......
if (NCPU > 1 && bound == 0 &&U.compareAndSwapInt(this, BOUND, 0, SEQ))
arena = new Node[(FULL + 2) << ASHIFT];
- 在
slotExchange()
方法中存在竞争时,会构建 arena。- 初始化 arena 时会设置 bound 为
SEQ(SEQ=MMASK + 1)
,255 + 1 = 256。 - NCPU 为到 Java 虚拟机可用的处理器数量。
Runtime.getRuntime().availableProcessors()
。 - 假设 NCPU 为 2,则 arena 数组大小为 384(2 >>> 1 然后 (1+2) << 7)。
- 初始化 arena 时会设置 bound 为
private static final sun.misc.Unsafe U;
private static final int ABASE;
U = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Node[].class;
s = U.arrayIndexScale(ak);
ABASE = U.arrayBaseOffset(ak) + (1 << ASHIFT);
FULL 和 ASHIFT 的定义
- arena 数组很大,但里面并不是每个位置都被使用了,还有一些是没有使用的。
- 通过 Unsafe 的
arrayBaseOffset(ak)
方法可以返回 arena 数组中第一个元素的偏移地址。 - 通过
arrayIndexScale(ak)
方法可以返回 arena 数组中每一个元素占用的大小,也就是元素与元素之间的间隔,即1 << ASHIFT
为 128。ABASE = arrayBaseOffset + (1 << ASHIFT)
是 arena 的起始位置加上 128 位这个偏移量。- arena 实际使用了 ABASE 做为起始位置,那么其前 128 位的位置都是没有使用的。
- 那么要访问 arena 的第 N 个元素(结点),偏移量 offset 为
arrayBaseOffset + N * arrayIndexScale
。
@sun.misc.Contended
注解 和1 << ASHIFT
主要是用于避免 伪共享。1 << ASHIFT
可以避免两个 Node 在同一个共享区(缓存行)。- 主流缓存行大小一般为 32 字节到 256 字节,128 个地址位基本覆盖到了常见的处理器平台。
- arena 数组中元素(结点)的分布间隔为 128 个整数倍地址位,也就是说最小相差 128 个地址位。
- 通过 Unsafe 的
Node q = (Node)U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE);
if (q != null && U.compareAndSwapObject(a, j, q, null)) {
Object v = q.item; // 获取槽位中结点 q 的数据
q.match = item; // 把当前线程的数据交换给它
Thread w = q.parked; // 获得槽位中结点 q 对应的线程对象
if (w != null)
U.unpark(w); //唤醒该线程
return v;
}
bound 和 collides
- bound 是上一次记录的 Exchanger.bound。
- bound 会记录 最大有效 的 arena 索引,是动态变化的,竞争激烈时(槽位全满)增加, 槽位空旷时减小。
- bound + SEQ 确立其唯一性(版本),低 8 位记录 有效索引。
- collides 是在当前 bound 下 CAS 失败的次数。
- 最大为 m,m(bound & MMASK)为当前 bound 下最大有效索引。
- 槽位最大值为 MMASK(255),bound 最大值也就是 255,m 和 i 的范围为 [0,255]。
- 从右往左遍历,等到 collides == m 时,有效索引的槽位已经遍历完,这时需要增长槽位。
- 增长的方式是重置 bound(依赖 SEQ 更新其版本,低位 + 1),同时 collides 重置。
private static final int MMASK = 0xff;
private static final int SEQ = MMASK + 1;
......
// MASK: 00000000000000000000000011111111
// SEQ: 00000000000000000000000100000000(MASK + 1)
// 1: 00000000000000000000000000000001
if (NCPU > 1 && bound == 0 && U.compareAndSwapInt(this, BOUND, 0, SEQ))
// 当 bound 为 0 时,bound 被更新为 SEQ
//第一次更新
//b0: 00000000000000000000000100000000
U.compareAndSwapInt(this, BOUND, b, b + SEQ + 1)
//SEQ+1: 00000000000000000000000100000001
//b0+SEQ+1=b1: 00000000000000000000000200000001
//第二次更新
//b1+SEQ: 00000000000000000000000300000001
//第二次是 -1 的情况
U.compareAndSwapInt(this, BOUND, b, b + SEQ - 1)
//b1+SEQ-1=b2: 00000000000000000000000300000000
- bound + SEQ 是版本递增的过程,
b + SEQ + 1
后再b + SEQ - 1
,实际经历了两个版本,并且会将 collides 重置。 - 下图中去除了实际存在的未使用位置,只保留了数组中被使用的位置。
- 其中被使用的位置数量最大值为 MMASK(255),FULL <= MMASK。
- 当前线程进入 " 第一个 " 槽位,发现有其它线程在交换数据,则增加 1 个槽位并且 bound 递增,此时最大有效索引为 1。
- m 等于 1,i 范围为 [0,1],p.index 等于 1。
- 当前线程进入后续槽位(包含之前增加的槽位),如果发现同样有其它线程在交换数据,则继续增加槽位,bound 递增。
- 当前线程进入后续槽位(包含之前增加的槽位),没有元素(结点),则尝试占据该槽位,占据成功则等待其它线程。
- 当等待超时则删除该槽位,再次从头开始遍历有效索引,寻找其它线程交换数据。
- bound 版本唯一性的作用主要用于更新索引,将有效索引更新到最右侧位置,使得可以再次从右向左(从头)遍历。
- 如果没有 bound 的版本唯一性,便没有索引更新,就会一直往左遍历竞争激烈的槽位。
- 如果没有 bound 的版本唯一性,还会使得 bound 只增不减,影响效率。
3. 总结
- 当前线程 A 和其它线程 B(一个或多个)在槽中交换数据。
- 单槽方法(slotExchange)执行,A 发现 B 已经在槽中,则尝试交换数据,如果成功,则进入第 2 步骤。如果失败则说明有其它线程已经在和 B 进行数据交换,则进入第 5 步骤。
- 交换数据成功,则交换结束。也可能超时或者中断,造成交换失败,只能从头开始。
- 到达槽位,未发现其它线程,则尝试占位,抢占成功,则自旋等待其它线程交换数据,进入第 4 步骤。抢占失败,则说明被其它线程抢占了槽位,则进入第 5 步骤。
- 其它线程来交换数据,成功则交换结束。如果等待超时则寻找其它线程进行交换,先删除一个槽位,再从头开始寻找其它线程交换数据。也有可能会被中断。
- 转为多槽方法(arenaExchange)执行,挨个寻找槽中是否有可交换数据的对象,如果发现交换对象且尝试交换数据成功,则进入第 2 步骤。如果为空槽,则占据并等待其它线程来交换数据,进入第 4 步骤。
- 尝试多次交换都未成功,则增加槽位,然后再从头开始。
参考资料
https://blog.csdn.net/carson0408/article/details/79477280
https://blog.csdn.net/u014634338/article/details/78385521
https://blog.csdn.net/chenssy/article/details/72550933
https://www.cnblogs.com/d-homme/p/9387948.html
https://www.cnblogs.com/aniao/p/aniao_exchanger.html