说说 Exchanger 原理
当一个线程到达 exchange 调用点时,如果它的伙伴线程此前已经调用了此方法,那么它的伙伴会被调度唤醒并与之进行对象交换,然后各自返回。如果它的伙伴还没到达交换点,那么当前线程将会被挂起,直至伙伴线程到达——完成交换正常返回;或者当前线程被中断——抛出中断异常;又或者是等候超时——抛出超时异常。
这段描述覆盖了 Exchanger 的行为面,但原理面还有三层值得展开:两两配对的槽位(slot)机制、为什么设计成多槽数组、以及它真正发光的生产者-消费者场景。
核心语义:两线程的会合点(rendezvous)
Exchanger 是点对点双向数据交换:两个线程在 exchange 点会合,各自把对象交给对方后继续执行。与队列的”单向传递”不同,交换是原子互换——双方都拿到对方的数据。
Exchanger<List<Record>> ex = new Exchanger<>(); |
实现机制:槽位数组 + 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 等待与撤销。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 非鱼小站!
评论
WalineDisqus











