背景(来自注释):
Exchanger的作用是作为一个同步点,线程通过它可以配对交换数据。
每个线程可以通过exchange方法来传递想要传递的数据,并且返回时接受其他线程传递的数据。一个Exchanger可以看做是双向的SynchronousQueue,经常在遗传算法和管道设计中使用。
使用场景:
下面这个FillAndEmpty类的亮点是,使用Exchanger在两个线程之间交换数据,使得填缓存的线程能够在需要时候得到一个新的空缓存,然后挂起自己直到清缓存的线程清空缓存。
class FillAndEmpty {
Exchanger exchanger = new Exchanger();
DataBuffer initialEmptyBuffer = ... a made-up type
DataBuffer initialFullBuffer = ...
class FillingLoop implements Runnable {
public void run() {
DataBuffer currentBuffer = initialEmptyBuffer;
try {
while (currentBuffer != null) {
addToBuffer(currentBuffer);
if (currentBuffer.isFull())
currentBuffer = exchanger.exchange(currentBuffer);
}
} catch (InterruptedException ex) { ... handle ... }
}
}
class EmptyingLoop implements Runnable {
public void run() {
DataBuffer currentBuffer = initialFullBuffer;
try {
while (currentBuffer != null) {
takeFromBuffer(currentBuffer);
if (currentBuffer.isEmpty())
currentBuffer = exchanger.exchange(currentBuffer);
}
} catch (InterruptedException ex) { ... handle ...}
}
}
void start() {
new Thread(new FillingLoop()).start();
new Thread(new EmptyingLoop()).start();
}
}}
算法的核心是通过一个可交换数据的slot,以及一个可以带有数据item的参与者:
for (;;) {
if (slot is empty) { // offer
place item in a Node;
if (can CAS slot from empty to node) {
wait for release;
return matching item in node;
}
}
else if (can CAS slot from node to empty) { // release
get the item in node;
set matching item in node;
release waiting thread;
}
// else retry on CAS failure
}
这种方式在原则上是高效的,但实际上,像许多在单个位置上进行原子更新的算法一样,当许多参与者使用同一个Exchanger时,存在严重的伸缩性问题。所以我们的实现通过引入一个消去数组从而安排不同的线程使用不同的slot来降低竞争,并且保证最终会成对交换数据。这意味着我们不能完全地控制线程之间的划分方式,但是我们通过在竞争激烈时增加arena的范围,竞争变少时减少arena的范围来分配线程可用的下标。我们为了达到这个效果,通过ThreadLocals来定义Node,以及在Node中包含了线程下标index以及相应的跟踪状态。(对于每个线程,我们可以重用私有的Node而不是重新创建一个,因为slot只会通过CAS操作交替地变化(Node VS null),从而不会遇到ABA问题。当然,我们在使用时需要重新设置item)。
为了实现一个高效的arena(场地),我们仅在探测到竞争时才开辟空间(当然在单CPU时我们什么都不做),首先我们通过单个slot的slotExchange方法来交换数据,探测到竞争时,我们会通过arena安排不同位置的slot,并且保证没有slot会在同一个缓存行上(cache line)。因为当前没有办法确定缓存行的尺寸,我们使用对任何平台来说都足够大的值。并且我们通过其他手段来避免错误/非意愿的共享来增加局部性,比如对Node使用边距(via sun.misc.Contended),"bound"作为Exchanger的属性,以及使用区别于LockSupport的重新安排park/unpark的版本来工作。
一开始我们只使用一个slot,然后通过记录冲突次数从而扩展arena的尺寸(冲突:CAS在交换数据时失败)。对于以上算法来说,能够表明竞争的碰撞类型是两个线程尝试释放Node的冲突----线程竞争提供Node的情况是可以合理地失败的。当一个线程在当前arena的每个位置上都失败时,会试着去递增arena的尺寸。我们记录冲突的过程中会跟踪"bound”的值,以及会重新计算冲突次数在bound的值被改变时(当然,在冲突次数足够时会改变bound)。
当arena的可用尺寸大于1时,参与者会在等待一会儿之后在退出时递减arena的可用尺寸。这个“一会儿”的时间是经验决定的。我们通过借道使用spin->yield->block的方式来获得合理的等待性能--在一个繁忙的exchanger中,提供数据的线程经常是被立即释放的,相对应的多cpu环境中的上下文切换是非常缓慢/浪费的。arena上的等待去除了阻塞,取而代之的是取消。从经验上看所选择的自旋次数可以避免99%的阻塞时间,在一个极大的持久的交换速率以及一系列的计算机上。自旋以及退让产生了一些随机的特性(使用一种廉价的xorshift)从而避免了严格模式下会引起没必要的增长/收缩环。对于一个参与者来说,它会在slot已经改变的时候知道将要被唤醒,但是直到match被设置之后才能取得进展。在这段时间里它不能取消自己,所以只能通过自旋/退让来代替。注意:可以通过避免第二次检测,通过改变线性化点到对于match属性的CAS操作,当然这样做会增加一点异步性,以及牺牲了碰撞检测的效果和重用一线程一Node的能力。所以现在的方案更好。
检测到碰撞时,下标逆方向循环遍历arena,当bound改变时会以最大下标(这个位置Node是稀少的)重新开始遍历(当退出(取消)时,下标会收缩一半直到变0)。我们这里使用了单步来遍历,原因是我们的操作出现的极快在没有持续竞争的情况下,所以简单/快速的控制策略能够比精确但是缓慢的策略工作地更好。
因为我们使用了退出的方式来控制arena的可用尺寸,所以我们不能直接在限时版本中抛出TimeoutException异常直到arena的尺寸收缩到0(也就是说只要下标为0的位置可用)或者arena没有被激活。这可能对原定时间有延迟,但是还是可以接受的。
本质上来看所有的实现都存于方法slotExchange和arenaExchange中。这两个有相似的整体结构,但是在许多细节上不同。slotExchange方法使用单个"slot"属性相对于使用arena中的元素。同时它也需要极少的冲突检测去激活arena的构造(这里最糟糕的部分是确定中断状态以及InterruptedException异常在其他方法被调用时出现,这里是通过返回null来检查是否被中断.)
在这种类型的代码中,由于方法依赖的大部分逻辑所读取的变量是通过局部量来维持的,所以方法是大片的并且难以分解--这里主要是通过连在一起的 spin->yield->block/cancel 代码)。以及严重依赖于本身具有的Unsafe机制和内联的CAS操作和相关内存读取操作。注意Node.item不是volatile的,尽管它会被释放线程所读取,因为读取操作只会在CAS操作完成之后才发生,以及所有自己的变量都是以能被接受的次序被其他操作所使用。当然这里也可以使用CAS操作来用match,但是这样会减慢速度)。
实现:
@sun.misc.Contended static final class Node {
int index; // Arena index
int bound; // Last recorded value of Exchanger.bound
int collides; // Number of CAS failures at current bound
int hash; // Pseudo-random for spins
Object item; // This thread's current item
volatile Object match; // Item provided by releasing thread
volatile Thread parked; // Set to this thread when parked, else null
}
/** The corresponding thread local class */
static final class Participant extends ThreadLocal {
public Node initialValue() { return new Node(); }
}
/**
* Per-thread state
*/
private final Participant participant;
/**
* Elimination array; null until enabled (within slotExchange).
* Element accesses use emulation of volatile gets and CAS.
*/
private volatile Node[] arena;
/**
* Slot used until contention detected.
*/
private volatile Node slot;
slot即为单个槽,arena为数组槽。
static final int FULL = (NCPU >= (MMASK << 1)) ? MMASK : NCPU >>> 1;
arena = new Node[(FULL + 2) << ASHIFT];
这里的ASHIFT为7,比如我的FULL=2(当前电脑4核),那么这里就是创建了的arena长度为512。
这是为何呢?我们再看如何使用arena中的Node。
Node q = (Node)U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE);
这里的ABASE是:
U = sun.misc.Unsafe.getUnsafe();
Class> ak = Node[].class;
ABASE = U.arrayBaseOffset(ak) + (1 << ASHIFT);
在当前的4核CPU下,arena可以用如下图来表示:
由于最大的缓存行为128个字节,并且Node有sun.misc.Contended来增加padding,所以使得任意两个可用Node不会再同一个缓存行中。
我们接下来看函数调用:
@SuppressWarnings("unchecked")
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);
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;
}
事实上这个限时版本存在一个微妙的bug。注意在slotExchange和arenaExchange中的时间参数ns是同一个,假如一次调用先经过slotExchange,再经过arenaExchange,那么就很可能发生等待时间过长的情况,原因是:U.park(false, ns)这个操作存在莫名其妙返回的可能,根据注释:*
Exchanger
slot:初始以及无竞争状态下使用;
arena[]:竞争状态下使用;
participant:线程独有Node;
/**
* Exchange function used until arenas enabled. See above for explanation.
*
* @param item the item to exchange
* @param timed true if the wait is timed
* @param ns if timed, the maximum wait time, else 0L
* @return the other thread's item; or null if either the arena
* was enabled or the thread was interrupted before completion; or
* TIMED_OUT if timed and timed out
*/
private final Object slotExchange(Object item, boolean timed, long ns) {
Node p = participant.get();
Thread t = Thread.currentThread();
if (t.isInterrupted()) // preserve interrupt status so caller can recheck
return null;
for (Node q;;) {
if ((q = slot) != null) {
if (U.compareAndSwapObject(this, SLOT, q, null)) {
Object v = q.item;
q.match = item;
Thread w = q.parked;
if (w != null)
U.unpark(w);
return v;
}
// create arena on contention, but continue until slot null
if (NCPU > 1 && bound == 0 &&
U.compareAndSwapInt(this, BOUND, 0, SEQ))
arena = new Node[(FULL + 2) << ASHIFT];
}
else if (arena != null)
return null; // caller must reroute to arenaExchange
else {
p.item = item;
if (U.compareAndSwapObject(this, SLOT, null, p))
break;
p.item = null;
}
}
// await release
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)) == 0)
Thread.yield();
}
else if (slot != p)
spins = SPINS;
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)) {
v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null;
break;
}
}
U.putOrderedObject(p, MATCH, null);
p.item = null;
p.hash = h;
return v;
}
/**
* Exchange function when arenas enabled. See above for explanation.
*
* @param item the (non-null) item to exchange
* @param timed true if the wait is timed
* @param ns if timed, the maximum wait time, else 0L
* @return the other thread's item; or null if interrupted; or
* TIMED_OUT if timed and timed out
*/
private final Object arenaExchange(Object item, boolean timed, long ns) {
Node[] a = arena;
Node p = participant.get();
for (int i = p.index;;) { // access slot at i
int b, m, c; long j; // j is raw array offset
Node q = (Node)U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE);
if (q != null && U.compareAndSwapObject(a, j, q, null)) {
Object v = q.item; // release
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; // offer
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; // xorshift
if (h == 0) // initialize hash
h = SPINS | (int)t.getId();
else if (h < 0 && // approx 50% true
(--spins & ((SPINS >>> 1) - 1)) == 0)
Thread.yield(); // two yields per wait
}
else if (U.getObjectVolatile(a, j) != p)
spins = SPINS; // releaser hasn't set match yet
else if (!t.isInterrupted() && m == 0 &&
(!timed ||
(ns = end - System.nanoTime()) > 0L)) {
U.putObject(t, BLOCKER, this); // emulate LockSupport
p.parked = t; // minimize window
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) // try to shrink
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; // clear offer
}
else {
if (p.bound != b) { // stale; reset
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)) {
p.collides = c + 1;
i = (i == 0) ? m : i - 1; // cyclically traverse
}
else
i = m + 1; // grow
p.index = i;
}
}
}
存在的问题: