交换者Exchanger完全解析
1 前言
Exchanger(交换者)是一个用于线程间协作的工具类, 它可以在配对线程中配对并交换数据。每个线程都可以在exchange入口方法上携带数据,然后与相应的线程进行匹配,并在返回时接收配对线程的数据。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。交换器可能在遗传算法和管道设计等应用场景中有很大用处。(基于JDK1.8)
2 示例
下面是一个交换缓冲区的示例,FillingLoop
向一个缓冲区中填充数据,若缓冲区已满就将当前的缓冲区交换出去获取一个新的未满的缓冲区,EmptyingLoop
class FillAndEmpty { Exchanger<DataBuffer> exchanger = new Exchanger<DataBuffer>(); 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(); } }
Exchanger也可以用于校对工作 ,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否录入一致.
class ExchangerTest { private static final Exchanger<String> exgr = new Exchanger<String>(); private static ExecutorService threadPool = Executors.newFixedThreadPool(2); public static void main(String[] args) { threadPool.execute(() -> { try { String A = "银行流水A";// A录入银行流水数据 exgr.exchange(A); } catch (InterruptedException e) { } }); threadPool.execute(() -> { try { String B = "银行流水B";// B录入银行流水数据 String A = exgr.exchange(B); System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:" + A + ",B录入是:" + B); } catch (InterruptedException e) { } }); threadPool.shutdown(); } }
3 源码分析
(1) 字段
//node数组,存储每个线程 待交换数据 的相关信息 private volatile Node[] arena; // 封装两个线程时的数据交换信息 private volatile Node slot; //高24位是版本号,低8位是arena数组的最大有效索引(元素为null不计数为有效索引) private volatile int bound; //arena每个元素之间的间隔位移255 (1<<7) private static final int ASHIFT = 7; //数组arena的最大容量 private static final int MMASK = 0xff; //bound的版本号 ,bound每次修改一次,其二进制的倒数第9位加1 private static final int SEQ = MMASK + 1;//二进制 0b1 0000 0000 //cpu个数,用来确定自旋次数 private static final int NCPU = Runtime.getRuntime().availableProcessors(); //arena的最大下标,表示所有线程无竞争或arena的最大可用的下标 static final int FULL = (NCPU >= (MMASK << 1)) ? MMASK : NCPU >>> 1; //自旋次数 private static final int SPINS = 1 << 10; //交换null数据时返回的对象。 private static final Object NULL_ITEM = new Object(); //交换数据超时返回的对象。 private static final Object TIMED_OUT = new Object(); //线程本地变量,继承于ThreadLocal private final Participant participant;
bound
:bound的高24位相当是版本号,每添加或删除一个node元素,bound的高24位都将加1;而bound的低8位表示arena数组的最大序数索引(p.index
的最大值),添加一个Node元素,低8位加1,删除一个Node元素,低8位减1.
为了防止伪共享,arena中的元素不是连接分布的,每个元素间隔(1<<ASHIFT)个下标长度,假设一个node元素的有效索引为i,那么(i<<ASHIFT)才是实际的数组下标。
(2) 内部类
@sun.misc.Contended static final class Node { //在arena数组中的下标 int index; // Arena index //上次记录的bound int bound; // Last recorded value of Exchanger.bound //CAS失败的次数 int collides; // Number of CAS failures at current bound //计数自旋次数会用到的随机码 int hash; // Pseudo-random for spins //当前线程需要交换的item. Object item; // This thread's current item //匹配线程传过来的item。 volatile Object match; // Item provided by releasing thread //当前休眠的线程,在被唤醒后设为null volatile Thread parked; // Set to this thread when parked, else null } //线程本地变量,记录每个线程对应的node /** The corresponding thread local class */ static final class Participant extends ThreadLocal<Node> { public Node initialValue() { return new Node(); } }
(3) 构造方法
构造方法只是简单地将participant实例化了
public Exchanger() { participant = new Participant(); }
(4) 方法exchange
Exchanger只有两个重载的公共方法exchange(V )
、exchange(V , long, TimeUnit )
,前者不限定阻塞时长,而后者要限定阻塞时长(超时版本).
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
和arenaExchange
,这里以exchange(V , long, TimeUnit )
为例作简单说明。
public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException { Object v; Object item = (x == null) ? NULL_ITEM : x;//x为空,将其转为NULL_ITEM。 long ns = unit.toNanos(timeout); if ((arena != null || //arena为空,未被初始化,执行slotExchange (v = slotExchange(item, true, ns)) == null) && ((Thread.interrupted() || //若线程中断就会抛出InterruptedException异常 (v = arenaExchange(item, true, ns)) == null))) //slotExchange返回null或arean不为null,执行arenaExchange。 throw new InterruptedException(); if (v == TIMED_OUT) throw new TimeoutException(); return (v == NULL_ITEM) ? null : (V)v; }
exchange的主要逻辑是:若arena为空,就执行slotExchange, 若arena不为空,就执行slotExchange。若slotExchange返回空,还将执行slotExchange。
(5) 基本算法
在分析slotExchange
和arenaExchange
方法之前,我们要先了解exchange的算法原理,正好Exchanger内部对其算法有些注释说明,下面是它的基本算法:
for (;;) { if (slot is empty) { // offer 第一个线程执行exchange,添加slot 阻塞等待 //slot是空的,就将构造一个Node节点,(将待交换值item包装成一个Node) place item in a Node; if (can CAS slot from empty to node) { //使用CAS成功将slot设为刚构造的node结点 //当前线程休眠等待,直到被唤醒 wait for release; //被唤醒后返回node中匹配的数据slot.match return matching item in node; } } //slot不为空, CAS成功将slot中设为null else if (can CAS slot from node to empty) { // release 第二个线程执行exchange,获取slot中的值。 唤醒等待线程 //获取slot中的item,(这个item是第二个线程要返回的值) get the item in node; //将待交换的item设置到slot.match中 set matching item in node; //唤醒等待的线程 release waiting thread; } // else retry on CAS failure 其他条件需要CAS自旋重试 }
其大致流程是:第一个线程执行首次exchange方法时,检测到slot为空,它就用自己的item构造一个Node节点,将这个node节点设为slot,然后阻塞等待直到第二线程将它的item传过来。第二个线程执行exchange方法,检测到slot不为空,它就获取到了slot中的item,这个item就是第一个线程传来的,现在第二个线程将自己的item设到slot.match
中,然后唤醒第一个线程。第一个线程被唤醒后返回第二个线程中设置的slot.match, 第二个线程返回第一个线程中设置的slot.item.
这种数据交换在两个线程中实现起来比较简单,但如果有3个以上的线程需要数据交换就比较麻烦了,这里就需要引入arena
数组来保存多个数据项了。
(6) 方法slotExchange
slotExchange方法的主要逻辑:
for循环自旋: slot不为空,设置slot的匹配数据,唤醒等待的线程,返回slot.item;slot为空,就将线程局部变量p通过CAS初始化赋值给slot,退出for循环;若arena不为空,就返回null。
while循环:先自旋SPINS次,然后休眠当前线程,直到被配对线程唤醒。
private final Object slotExchange(Object item, boolean timed, long ns) { Node p = participant.get(); Thread t = Thread.currentThread(); //外部方法exchange的if语句中的Thread.interrupted()能重置中断,将其设为非中断的 ,外部的exchange方法将抛出中断异常 if (t.isInterrupted()) // preserve interrupt status so caller can recheck return null; //第一次自旋,初始化slot和arena for (Node q;;) { //q代表当前的slot if ((q = slot) != null) { //slot不为空, //CAS将slot设空, //获取并返回对方线程传入的交换数据q.item //将当前线程待交换数据item设到匹配项q.match, //唤醒对方线程(对方线程在被唤醒后将返回q.match) 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; } //CAS失败,有线程竞争,初始化arena // create arena on contention, but continue until slot null if (NCPU > 1 && bound == 0 && U.compareAndSwapInt(this, BOUND, 0, SEQ))//将bound的倒数第9设为1,bound低8位全是零 arena = new Node[(FULL + 2) << ASHIFT]; } else if (arena != null) //slot为空和arena不为空,返回空,然后将执行arenaExchange方法 return null; // caller must reroute to arenaExchange else { //slot和arean均为空,第一个线程首次执行exchange方法,就是这种情况 //将待交换的数据放在当前线程变量p中, p.item = item; //CAS设值 if (U.compareAndSwapObject(this, SLOT, null, p)) break;//CAS成功了,进入下面的自旋等待 //CAS失败,可能其他线程先于当前线程CAS设值成功,可以获取另外其他线程传的item. //重新将p.item设为null,为下次自旋准备 p.item = null; } } // await release int h = p.hash; long end = timed ? System.nanoTime() + ns : 0L; //超时的截止时间 int spins = (NCPU > 1) ? SPINS : 1;//CPU数目只有一个时,不进行自旋,防止浪费有限的CPU资源 Object v; while ((v = p.match) == null) {//反复获取p.match,v不为空才返回 if (spins > 0) { h ^= h << 1; h ^= h >>> 3; h ^= h << 10;//异或操作将,h更随机 if (h == 0) //p.hash作为成员变量,初始值为0,将其初始化为非零的值 h = SPINS | (int)t.getId(); else if (h < 0 && (--spins & ((SPINS >>> 1) - 1)) == 0) //将自旋次数减1 //礼让其他线程 Thread.yield(); } else if (slot != p)//slot与p不等,表明slot被其他给修改了,需要重新自旋,重新最新的值 spins = SPINS; //自旋次数为0,不再自旋,准备休眠等待 else if (!t.isInterrupted() && arena == null && //未中断,arena不为空 (!timed || (ns = end - System.nanoTime()) > 0L)) {//未超时 U.putObject(t, BLOCKER, this); //记录当前线程阻塞的对象this p.parked = t;//阻塞的线程是当前线程t if (slot == p) U.park(false, ns);//当前线程休眠 //被唤醒后,将阻塞相关的属性重新设为null p.parked = null; U.putObject(t, BLOCKER, null); } //arena为null 或超时了或中断了(非正常状态),尝试将slot设为null(方法准备返回了) else if (U.compareAndSwapObject(this, SLOT, p, null)) { // CAS成功,退出自旋,。 v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null; break; } } //p.match被取出了,将其设为null U.putOrderedObject(p, MATCH, null); //item也设为null p.item = null; p.hash = h;//记录算出的hash return v; }
(7) 方法arenaExchange
arenaExchange与slotExchange方法很相似,只不过arenaExchange针对数组的node元素进行相应的处理而已。arenaExchange先要确定元素的下标位置,p.index是表示p在arena中的序数i(从0开始计数,表示第i个元素),arean中的元素不是连接分布的,每个元素间隔1<<ASHIFT个下标,所以i<<ASHIFT
才是第i个元素的下标。
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节点, // (1 << ASHIFT) 是每个node的索引间隔(防止伪共享) Node q = (Node)U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE); //因为要即将取走这个node中的item,所以CAS将arena中第i个有效元素设为null if (q != null && U.compareAndSwapObject(a, j, q, null)) { //CAS成功 //获取并返回对方线程传入的交换数据q.item //将当前线程待交换数据item设到匹配项q.match, //唤醒对方线程(对方线程在被唤醒后将返回q.match) Object v = q.item; // release q.match = item;//设置匹配item Thread w = q.parked; if (w != null) U.unpark(w);//唤醒对方线程 return v; } // (bound) & MMAS取bound的低8位,所以m最大索引 //q为null且索引位i小于等于最大索引 else if (i <= (m = (b = bound) & MMASK) && q == null) { p.item = item; // offer //将CAS设置arena数组中对应位置的node 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;;) { //返回对方传入匹配数据p.match,并将相应的数据清空 Object v = p.match; if (v != null) { //有匹配的数据 U.putOrderedObject(p, MATCH, null);//取出match后,将之清空 //将p.item清空,为下次做准备 p.item = null; // clear for next use p.hash = h;//记录hash return v; } else if (spins > 0) { //v为空,且还有剩余自旋次数 //异或操作将,h更随机 h ^= h << 1; h ^= h >>> 3; h ^= h << 10; // xorshift if (h == 0) // initialize hash //p.hash作为成员变量,初始值为0,将其初始化为非零的值 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 //自旋次数为0,准备休眠等待 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); } //其他情况,如中断了,m不为0,或超时了 (非正常状态) else if (U.getObjectVolatile(a, j) == p && U.compareAndSwapObject(a, j, p, null)) { if (m != 0) // try to shrink //bound加一个SEQ单位,”-1“表示最大有效索引减1,arena中少了一个元素 U.compareAndSwapInt(this, BOUND, b, b + SEQ - 1); p.item = null; p.hash = h; //i减半 i = p.index >>>= 1; // descend if (Thread.interrupted())//中断 return null; if (timed && m == 0 && ns <= 0L)//超时 return TIMED_OUT; break; // expired; restart } } } else//CAS设置arena数组中对应位置的node 失败 //清空p.item,将自旋重试 p.item = null; // clear offer } else { //q不为空,i大于最大的有效索引 if (p.bound != b) { // stale; reset 值b过期了,重新获取 p.bound = b; p.collides = 0;//cas失败次数为0 i = (i != m || m == 0) ? m : m - 1;//i取最大可用的索引(准备从尾向前遍历) } else if ((c = p.collides) < m || m == FULL || //”+1“表示最大有效索引加1,arena中多了一个元素 !U.compareAndSwapInt(this, BOUND, b, b + SEQ + 1)) { //失败次数加1 p.collides = c + 1; //循环遍历,(反向遍历到第一个元素之后,又跳到数组的最后一个元素) i = (i == 0) ? m : i - 1; // cyclically traverse } else //有效索引加1, i = m + 1; // grow p.index = i;//更新index } } } }
参考: 《 Java并发编程的艺术》方腾飞