bigywater.tech
Java · 230

Exchanger:两个线程如何交换中间结果

Exchanger 让两个线程在一个汇合点互换对象,典型用法是一个线程填满缓冲区、另一个线程消费空缓冲区。这篇真实运行它的交换、超时中断、多线程配对,并与阻塞队列对比传输开销。

引用
Exchanger 交换的是对象引用,不复制内容
13–56 ms
Exchanger 传 2048 万个 int(1024 一个缓冲区)
1950 ms
同样数据用 LinkedBlockingQueue 逐个传(中位)

两个线程交换对象

实测 · 本机执行输出
  主线程拿到「来自子线程」,子线程拿到「来自主线程」;子线程先到,等了 210 ms(约等于主线程晚到的 200 ms),交换完成后两边同时返回
  exchange(null) 是允许的:对方传 null,这边得到 null;反过来对方得到「x」
  交换的是引用不是内容:主线程送出数组 A,拿回 B(true),子线程拿到 A(true),没有复制;之后两边各自独占一个数组,A 的内容仍是 [1, 2, 3]

超时、中断与继续使用

实测 · 本机执行输出
  没有伙伴:exchange(x, 300ms) → TimeoutException,耗时 306 ms
  超时之后同一个 Exchanger 仍可用:主线程拿到「子线程的值」,子线程拿到「主线程的值」
  等待中的线程被中断 → InterruptedException;之后换一对线程交换:主线程得到 B,子线程得到 A
  伙伴晚到 400 ms,而超时 100 ms → TimeoutException(先到的一方放弃,不会被晚到的伙伴补上)

多于两个线程共用

实测 · 本机执行输出
  4 个线程(T0 先到,依次间隔 20 ms)共用一个 Exchanger,每个线程的结果:{T0=T1, T1=T0, T2=T3, T3=T2}
  5 个线程(T0 先到,依次间隔 20 ms)共用一个 Exchanger,每个线程的结果:{T0=T1, T1=T0, T2=T3, T3=T2, T4=(超时,没有伙伴)}
  4 个线程同时开始,重复 30 次,出现过的配对方式:[0↔1 2↔3, 0↔3 1↔2](同时到达时谁和谁配对不固定)

交换缓冲区 vs 阻塞队列

实测 · 本机执行输出
  Exchanger 交换 1024 元素的缓冲区:消费方求和 = 71680000(期望 71680000)
  第 1 轮,传 20480000 个 int:Exchanger 换缓冲区 56 ms;ArrayBlockingQueue 传缓冲区(回收池)65 ms;LinkedBlockingQueue 逐个传 2015 ms
  第 2 轮,传 20480000 个 int:Exchanger 换缓冲区 16 ms;ArrayBlockingQueue 传缓冲区(回收池)69 ms;LinkedBlockingQueue 逐个传 1899 ms
  第 3 轮,传 20480000 个 int:Exchanger 换缓冲区 54 ms;ArrayBlockingQueue 传缓冲区(回收池)62 ms;LinkedBlockingQueue 逐个传 2000 ms
  第 4 轮,传 20480000 个 int:Exchanger 换缓冲区 13 ms;ArrayBlockingQueue 传缓冲区(回收池)68 ms;LinkedBlockingQueue 逐个传 1688 ms

分步演示

Exchanger
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,10 核 macOS
    • 没有验证:Exchanger 内部的 arena 与槽位在高并发下的行为、虚拟线程下的表现、多对线程同时使用同一个 Exchanger 的扩展性;Exchanger 缓冲区交换耗时的两种取值(约 13–16 ms 与 52–56 ms)没有查原因;三种做法的耗时只是这台机器上的相对关系
    京ICP备2026067502号-1京公网安备11010502063487号