当一个线程到达 exchange 调用点时,如果它的伙伴线程此前已经调用了此方法,那么它的伙伴会被调度唤醒并与之进行对象交换,然后各自返回。如果它的伙伴还没到达交换点,那么当前线程将会被挂起,直至伙伴线程到达——完成交换正常返回;或者当前线程被中断——抛出中断异常;又或者是等候超时——抛出超时异常。

这段描述覆盖了 Exchanger 的行为面,但原理面还有三层值得展开:两两配对的槽位(slot)机制、为什么设计成多槽数组、以及它真正发光的生产者-消费者场景。

核心语义:两线程的会合点(rendezvous)

Exchanger 是点对点双向数据交换:两个线程在 exchange 点会合,各自把对象交给对方后继续执行。与队列的”单向传递”不同,交换是原子互换——双方都拿到对方的数据。

Exchanger<List<Record>> ex = new Exchanger<>();

// 生产线程:装满一批 → 交出空批换回
List<Record> buf = new ArrayList<>(BATCH);
while (running) {
buf.add(read());
if (buf.size() == BATCH) buf = ex.exchange(buf); // 满批换空批
}

// 消费线程:拿满批处理 → 交出空批
List<Record> buf = new ArrayList<>(BATCH);
while (running) {
buf = ex.exchange(buf); // 空批换满批
process(buf); buf.clear();
}

实现机制:槽位数组 + CAS + park/唤醒

JDK7 重写后的 Exchanger 内部是一个 slot 数组(类似 ConcurrentHashMap 的分槽思想):

  • 每个 slot 是一个带状态的字(item + bound 线程 + 状态位),线程先 CAS 占据一个空槽”放下自己的对象并挂起等待”
  • 后到的线程扫描槽位,发现已占用的槽则 CAS 取走对方 item、填入自己的 item 并唤醒对方——配对成功,互换完成
  • 竞争剧烈时线程分散到不同 slot(按竞争计数递增索引),减少同一槽的 CAS 冲突;空闲时收敛到 0 号槽提高配对命中率
  • 等待实现:自旋若干次后 park 阻塞;中断/超时则 CAS 撤销自己在槽中的占位并抛异常

多槽设计的动机与 LongAdder/CHM 一致:把单点竞争分散为多点并行,高竞争下吞吐不崩塌。

三种返回路径

  • 正常配对:双方原子互换对象返回
  • 中断:抛 InterruptedException,槽位撤销
  • 超时(exchange(v, timeout, unit)):抛 TimeoutException——限时会合的必备写法,防止伙伴线程死亡导致永久挂起

典型场景与边界

适用:

  • 双缓冲交换:生产者填缓冲、消费者清缓冲,每轮交换缓冲区所有权(如上例),避免每元素入队出队的开销
  • 遗传算法/并行计算:两个工作线程 periodically 交换种群/边界数据
  • 对账核对:两方各自计算结果后交换比对

边界与误用:

  • Exchanger 严格两两配对:三个线程共用一个 Exchanger 时,每轮只有两个配对,第三个等下一轮——多人协作应选 CyclicBarrier
  • 伙伴不出现就挂起:单方调用 exchange 而无对应方,线程阻塞到超时/中断——必须设计好配对生命周期或带超时
  • 交换的是引用:双方拿到对方对象后对可变对象的修改互相可见,必要时交换不可变快照

与同类原语对比

原语 传递方向 参与者 语义
SynchronousQueue 单向 2 交接(不给就拿不到)
Exchanger 双向互换 2 会合+交换
CyclicBarrier 无数据 N 到齐放行
BlockingQueue 单向 任意 缓冲传递

Exchanger 是并发工具箱里最”小众”但语义最精确的一件:当问题真的是”两个线程周期性互换缓冲区”时,它比队列+锁的组合更简洁也更快。原理记住三点即可:槽位配对、CAS 互换、park 等待与撤销。