Exchanger 原理

候选人小秦在面试阿里 P7 时,面试官问道:

"你知道 Exchanger 吗?它有什么用?"

小秦说:"用于线程间交换数据..."面试官追问:"它是怎么实现的?为什么叫槽位交换?"

小秦答不上来。面试官继续:"Exchanger 在高并发下有什么性能问题?"

小秦彻底卡住了...

一、核心问题:Exchanger 原理 🟡

1.1 问题拆解

第一层:使用场景(是什么?)
  "Exchanger 的典型使用场景是什么?"
  考察点:线程间数据交换、双缓冲、无锁队列

第二层:槽位机制(怎么做到?)
  "Exchanger 是怎么实现配对的?槽位和链表的作用?"
  考察点:槽位状态、CAS 配对、自旋

第三层:性能问题(有什么限制?)
  "Exchanger 在高并发下有什么性能问题?"
  考察点:自旋消耗、NUMA 架构、吞吐量

1.2 ❌ 错误示范

候选人原话 A:"Exchanger 和 SynchronousQueue 是一样的,都是一个线程给另一个线程传数据。"

问题诊断:两者有区别。SynchronousQueue 是即时交换——put 必须等待 take,take 必须等待 put。Exchanger 是延迟交换——线程先把数据放到槽位,等另一个线程来取,同时把自己的数据放到槽位。

候选人原话 B:"Exchanger 可以配对多个线程。"

问题诊断:Exchanger 一次只配对两个线程。多于两个线程同时交换时,需要排队。

1.3 标准回答

P5 级别:使用场景

典型场景:生产者-消费者之间的数据交换

Exchanger<Data> exchanger = new Exchanger<>();

// 线程 A:生产者
Data dataA = produce();
Data receivedA = exchanger.exchange(dataA);  // 等待线程 B 交换

// 线程 B:消费者
Data dataB = new Data();
Data receivedB = exchanger.exchange(dataB);  // 等待线程 A 交换

// 交换完成后:
// 线程 A 获得线程 B 的 dataB
// 线程 B 获得线程 A 的 dataA

常见使用场景

  • 双缓冲:一个线程填充缓冲区,另一个线程消费缓冲区,交换角色
  • 并行计算:两个线程各自计算一半结果,然后交换并继续
  • 无锁队列:单生产者单消费者场景,用 Exchanger 减少同步开销

P6 级别:槽位机制

核心实现

Exchanger 使用一个槽位(slot)和一个等待队列(Node 链表)实现配对:

// Exchanger 的核心数据结构
private final Exchanger.Node[] arena = new Exchanger.Node[CAPACITY];
private volatile Exchanger.Node slot;

// Node:等待节点
static final class Node {
    final Object item;          // 携带的数据
    final int mode;             // WAITING=0, MATCH=1, CANCELLED=-1
    volatile Exchanger.Node next;  // 链表指针
}

exchange() 的流程

public V exchange(V x) throws InterruptedException {
    Exchanger.Node s = new Exchanger.Node(x, false);

    // 1. 尝试直接配对(单槽路径)
    Exchanger.Node me = slot;
    if (me != null && me.compareAndSetMode(...)) {  // 槽中有等待者
        // 配对成功:交换数据
        V you = me.getItem();
        me.setItem(x);  // 将自己的数据给对方
        me.compareAndSetWaiter(null);  // 标记配对完成
        LockSupport.unpark(me.waiter);
        return you;
    }

    // 2. 槽为空:将自己放入槽,等待配对
    slot = s;
    LockSupport.park(this);  // 自旋或阻塞等待

    // 3. 被唤醒后,自己的 item 已被对方替换
    return s.getItem();
}

为什么叫槽位交换?

槽位(slot)是一个共享位置,第一个到达的线程将自己的数据放入槽位并等待;第二个到达的线程看到槽位中有数据后,取走数据并放入自己的数据,唤醒第一个线程。两个线程通过槽位"交换"了数据。

P7 级别:性能问题与改进

Exchanger 的性能瓶颈

  1. 单槽竞争:当多个线程竞争同一个 slot 时,CAS 失败导致自旋消耗 CPU
  2. NUMA 架构问题:在大型 NUMA 系统上,slot 可能位于某个节点的本地内存,导致其他节点访问延迟高
  3. 伪共享:arena 数组中的多个 Node 可能共享同一缓存行

JDK 的改进:arena(多槽)机制

// JDK 6+ 引入 arena,多个槽位减少竞争
private final Exchanger.Node[] arena = new Exchanger.Node[16];

// 线程首先尝试单槽路径(slot)
// 如果 slot 竞争激烈,移入 arena
// arena 中的槽位根据线程 ID 选择(散列)
int m = (1 + (NSOITS - 1)) & (h ^ (h >>> 15));

单槽 vs 多槽路径

  • 单槽路径(slot):无竞争时最快(无数组索引计算)
  • 多槽路径(arena):高竞争时减少 CAS 争用
  • 自旋阈值:JDK 使用启发式策略决定是否自旋或阻塞

【面试官心理】 这道题我能问到 P7 级别,是因为 Exchanger 的实现涉及了槽位机制、NUMA 架构、自旋优化等多个层次。能说清单槽和多槽路径切换的候选人说明他理解了性能优化思想。

1.4 追问升级

追问 1:Exchanger 和 SynchronousQueue 的区别?

维度ExchangerSynchronousQueue
数据交换线程 A 的数据 ↔ 线程 B 的数据线程 A 的数据 → 线程 B
持有时间线程 A 把数据放入槽后,等待 B 来取put 必须立即被 take 取走
阻塞方式slot + arena + parkTransferStack/TransferQueue
使用场景数据需要双向交换单向传递

追问 2:Exchanger 可以用于超过两个线程吗?

同一时刻一个 slot 只能配对一对线程。但多个线程可以轮流使用 Exchanger——线程 A 和 B 交换后,线程 A 可以继续与线程 C 交换。

二、生产避坑 🟢

2.1 Exchanger 不是万能的

Exchanger 只适用于两个线程之间的数据交换。对于 N 个线程的交换场景(如多个线程的结果汇总),应该用其他机制(如 CyclicBarrier、Phaser)。

2.2 中断处理

exchange() 响应中断,会抛出 InterruptedException。中断后的行为取决于具体实现。

三、Exchanger 的替代方案 🟢

3.1 双向 LinkedBlockingQueue

LinkedBlockingQueue<Data> q1 = new LinkedBlockingQueue<>(1);
LinkedBlockingQueue<Data> q2 = new LinkedBlockingQueue<>(1);

// 线程 A → 线程 B
q1.offer(data);

// 线程 B → 线程 A
q2.offer(otherData);

// 线程 A 获取
Data fromB = q2.poll();

// 相比 Exchanger:
// - 方向更明确
// - 容量可调
// - 但多了一个队列对象

3.2 PipedInputStream/PipedOutputStream

用于字节流方向的数据交换,但在 Java 中不常用(性能差、容量小)。

💡

面试加分点:能说出"Exchanger 的 arena 使用 2 的幂次长度,通过 (hash ^ (hash >>> 15)) & (length - 1) 实现散列选择槽位",说明他理解了 JDK 的优化策略。

⚠️

面试陷阱:被问到"Exchanger 的 exchange() 在超时后会怎样",很多人会说"抛异常"。准确答案是:exchange(x, timeout, unit) 在超时时返回 null(而不是抛异常),调用者需要检查返回值是否为 null。