JUC 之 Exchanger 详解
在一个同步点上,线程可以配对和交换成对中的元素。每个线程在进入exchange方法时携带自身数据,与另一线程进行匹配,配对成功时两个线程相互交换数据并返回对方的数据。交换器可以看作是双向形式的同步队列。
核心成员介绍
public Exchanger() {
participant = new Participant();
}
// 用于计算 arena 数组中两个可用数组项之间的间隔字节数,大小至少是一个缓存行的大小
private static final int ASHIFT = 7;
// arena 支持的最大索引个数 0xff = 1111 1111 = 255
private static final int MMASK = 0xff;
// bound 发生变更时递增 MMASK + 1 = 1 0000 0000 = 256,每次递增都是同时增加高 8 位和 低 8 位,即同时对 版本号 加 1,数组有效索引个数 加 1
private static final int SEQ = MMASK + 1;
private static final int NCPU = Runtime.getRuntime().availableProcessors();
// arena 所能容纳的最大有效索引个数
static final int FULL = (NCPU >= (MMASK << 1)) ? MMASK : NCPU >>> 1;
// 自旋次数
private static final int SPINS = 1 << 10;
// 保存数据的节点
private volatile Node[] arena;
// 当没有发生线程竞争时用于数据交换
private volatile Node slot;
// arena 数组最大有效位索引,每次更新 arena 时进行更新,初始为 0 ,保证 arena 数组只被初始化一次
private volatile int bound;
@sun.misc.Contended static final class Node {
int index; // node 所处 arena 数组的下标
int bound; // 记录 Exchanger 的 bound
int collides; // 记录 CAS 失败的次数
int hash;
Object item; // 保存带交换的数据
volatile Object match; // 保存其它线程交换成功的数据
volatile Thread parked; // 保存当前线程信息,用于交换线程唤醒使用
}
核心方法介绍
exchange 方法
public V exchange(V x) throws InterruptedException {
Object v;
Object item = (x == null) ? NULL_ITEM : x; // translate null args
if ((arena != null ||
(v = slotExchange(item, false, 0L)) == null) &&
((Thread.interrupted() || // disambiguates null return
(v = arenaExchange(item, false, 0L)) == null)))
throw new InterruptedException();
return (v == NULL_ITEM) ? null : (V)v;
}
slotExchange 方法
现成竞争不激烈的情况,通过一个 slot 操作就能完成匹配交换数据行为。当发生竞争激烈的时候,线程将会转移到 arena 数组的操作上。
所以在此方法中如果发现线程竞争激烈将要负责创建 arena 数组,数组创建完成后,需要唤醒那个正在等待 slot 交换的线程,让其也加入到对 arena 数组的操作上(arenaExchange 方法)。
private final Object slotExchange(Object item, boolean timed, long ns) {
Node p = participant.get();// 初始化一个 node 节点
Thread t = Thread.currentThread();
if (t.isInterrupted()) // 该方法检测线程是否被中断,不会清除中断标志位,当被中断时,返回 null,在 exchange 方法中将被检测到然后抛出 InterruptedException 异常
return null;
// 该 for(;;) 循环中存在一下几种情况
// 1、slot == null,代表你是第一个进入的线程,直接将携带的数据 保存到上面初始化的 p 中,然后将 slot 设置为 p,退出该循环等待其它线程过来交换数据
// 2、slot 以初始化,CAS 尝试进行数据交换,CAS 失败表明已被其它线程交换走了,也即线程竞争激烈,那么该线程将负责 初始化 arena 数组,然后该线程会进入 arenaExchange 方法中进行数据交换
for (Node q;;) {
if ((q = slot) != null) {// slot 已初始化,并将 slot 赋值给到 q,保存在栈上
if (U.compareAndSwapObject(this, SLOT, q, null)) {
// CAS 尝试将 slot 置空,成功表明该线程完成了匹配,下面进行数据交换操作
Object v = q.item;
q.match = item;// 将交换数据赋值给 match (待匹配线程)
Thread w = q.parked; // 判断 待匹配线程是否已经调用了 park 方法进入 OS 阻塞,是 唤醒之
if (w != null) U.unpark(w);
return v;// 匹配成功返回
}
// CPU 核心数大于 1,bound 变量用于控制 arena 数组只会被创建一次,
if (NCPU > 1 && bound == 0 &&
U.compareAndSwapInt(this, BOUND, 0, SEQ))// SEQ = 0x100,
arena = new Node[(FULL + 2) << ASHIFT];// 创建 arena 数组
}
else if (arena != null) return null; // arena != null 代表线程竞争激烈,应该进入 arenaExchange 方法进行数据匹配交换
else {// arena == null 尝试将 数据 放入 slot,CAS 失败说明有线程竞争,则重复for(;;)循环
p.item = item;
if (U.compareAndSwapObject(this, SLOT, null, p)) break;
p.item = null;
}
}
int h = p.hash;
long end = timed ? System.nanoTime() + ns : 0L;// 如果是超时等待,则计算超时等待时间
int spins = (NCPU > 1) ? SPINS : 1;// 自旋次数
Object v;
while ((v = p.match) == null) {// 数据暂未匹配成功,也即没有现成过来交换数据
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 & ((SPINS >>> 1) - 1)/*0x 1ff */) == 0)// spins 低 9 位等于 0 时让出 CPU 执行权
Thread.yield();
}
else if (slot != p) spins = SPINS;// 说明 slot 已经被匹配
else if (!t.isInterrupted() && arena == null && // 线程未被中断,且 arena 为 null(arena 不为 null时表示其它线程都去 arenaExchange 方法进行数据交换操作了,不会再有线程去操作 slot)
(!timed || (ns = end - System.nanoTime()) > 0L)) {
// 暂未有线程进来与之匹配交换数据,将进入 OS 阻塞
U.putObject(t, BLOCKER, this);// 通过 jstack 命令可看到 当前线程阻塞在哪个对象上
p.parked = t;// 保存线程信息,用于交换线程唤醒使用
if (slot == p)
U.park(false, ns);
p.parked = null;
U.putObject(t, BLOCKER, null);// 唤醒后清空 BLOCKER 对象
}
else if (U.compareAndSwapObject(this, SLOT, p, null)) {// 走到这里说明 arena 已经被创建,那么清空 slot ,让其进入 arenaExchange 方法完成数据交换操作
// 如果是因为超时导致的返回,设置返回值 v 为 TIMED_OUT = new Object(),否则设置为 null 并结束循环
v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null;
break;
}
}
// 交换成功,使用 putOrderedObject 优化 volatile 变量,只需保证 后面的写操作不会重排序即可,不需要保证 match 的可见性
U.putOrderedObject(p, MATCH, null);// 清空 match 的值
p.item = null;
p.hash = h;
return v;
}
arenaExchange 方法
当在多线程高并发的场景下,需要交换的数据过多,单个 slot 无法满足时将会切换到 arena 数组中进行数据交换,此时 slot 槽就没有存在的意义了,我们使用 arena 数组来承载更多的线程在无法交换数据时的阻塞等待操作。每个线程首次进入该方法时都会从各自的 ThreadLocal(participant) 中获取一个 node 节点信息,此时初始 index == 0,所以每个首次进来的线程都将去竞争 arena 数组中同一个索引槽的 node 信息,自然就会有线程竞争失败而导致重试,为了提升性能,每次重试并不是按照数组下标递增的方式寻找下一个可用数组项,而是向前跳过一定的数组项找到一个可用的项,也即打散每个线程所占用的数组项(因为数组是地址连续的,如果两个线程使用的数组项是挨在一起的,那么就有可能同时被加载到 CPU 的同一个缓存行中,这样就会导致两个线程并发操作时要对缓存行上锁从而降低了性能)
private final Object arenaExchange(Object item, boolean timed, long ns) {
Node[] a = arena;
Node p = participant.get();// 每个线程首次进入该方法获取到的 node 节点 index 默认都是从 0 开始
for (int i = p.index;;) { // access slot at i
int b, m, c; long j; // j is raw array offset
// 获取 arena 数组 i 处的数组项的 node 信息,此处使用的是 Unsafe 类的 getObjectVolatile 方法,其它线程修改了当前线程可以立即看到修改值。(通过 Unsafe 的 xxxVolatile 方法可以让一个普通变量拥有 volatile 语义)
Node q = (Node)U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE);
// q != null 说明有其它线程放入了数据,那么 CAS 尝试与之进行匹配交换,CAS 成功则 匹配成功
if (q != null && U.compareAndSwapObject(a, j, q, null)) {
Object v = q.item; // 获取 q 的 item 值
q.match = item; // 将自己的数据赋值给 q 的 match 属性
Thread w = q.parked;
if (w != null) U.unpark(w); // 如果待匹配线程以调用 park 方法去阻塞了,唤醒之
return v; // 当前线程返回
}
// 首次进入 for 循环 第一个判断恒成立,q == null 代表该位置还未被使用,那么尝试将自己的数据 放入其中,等待其它线程与之匹配
else if (i <= (m = (b = bound) & MMASK) && q == null) {
p.item = item;
// CAS 成功,位置抢占成功,下面代码处理逻辑同 slotExchange 方法,
if (U.compareAndSwapObject(a, j, null, p)) {
long end = (timed && m == 0) ? System.nanoTime() + ns : 0L;
Thread t = Thread.currentThread(); // wait
for (int h = p.hash, spins = SPINS;;) {
Object v = p.match;
if (v != null) { // 有线程与之匹配交换成功
U.putOrderedObject(p, MATCH, null);
p.item = null; // clear for next use
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 & ((SPINS >>> 1) - 1)) == 0)
// 达到某个特定值时 让出 CPU 执行权,
Thread.yield();
}
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)// m != 0 表示目前使用的数组项存在空余,尝试减少有效数组项的个数,
U.compareAndSwapInt(this, BOUND, b, b + SEQ - 1);
p.item = null;
p.hash = h;
i = p.index >>>= 1; // descend
if (Thread.interrupted())
return null;
if (timed && m == 0 && ns <= 0L)
return TIMED_OUT;
break; // expired; restart
}
}
}
else
p.item = null; // CAS 失败 清空 数据
}
else {
// 此 else 分支主要目的在于打散 arena 数组中的有效 slot 槽,
// 此时 p 的 bound 的值和当前 bound 值不一致,表明当前线程node 节点所在的 arena 的数组有效 slot 已经被更新,重新记录 bound 和 collides ,然后更新 i 值重试
if (p.bound != b) {
p.bound = b;
p.collides = 0;
i = (i != m || m == 0) ? m : m - 1;
}
else if ((c = p.collides) < m || m == FULL ||
!U.compareAndSwapInt(this, BOUND, b, b + SEQ + 1)) {// CAS 扩大有效索引项
p.collides = c + 1;
i = (i == 0) ? m : i - 1; // cyclically traverse
}
else
i = m + 1; // grow
p.index = i;
}
}
}
总结:通过源码我们看到在没有现成竞争的情况下通过一个 slot 槽就能完成数据交换操作,次过程在 slotExchange 方法中完成,当发生线程竞争时,在该方法中将会创建 arena 数组(后续数组中的每个数组项简称一个 slot 槽),通过 Exchanger 的 bound 变量控制数组只会被初始化一次。arena 数组初始化时候线程将从 该方法退出进而进入 arenaExchange 方法中去完成数据交换操作。arena 数组中的数组项并没有被全部用来做数据交换使用,而是使用 FULL 对其进行了限制,只能使用 FULL 个数组项(FULL = (NCPU >= (MMASK << 1)) ? MMASK : NCPU >>> 1,一般情况 FULL 等于 NCPU 个数 / 2 )。线程在使用 arena 数组中的 slot 槽时,每个线程所使用的 slot 是间隔开的,每个相邻的 slot 之间通过字节数填充,使其每个所占用的字节大小等于 CPU 缓存行的大小,避免多个 slot 被加载到同一个缓存行,进而导致性能下降。