导读:本期聚焦于泰国程序员创作的《Exchanger 变量交换:如何实现两个线程之间安全交换数据的同步点》,敬请观看详情。两个线程各自持有数据,想在某个时刻互换,怎么办?JUC 包里的 Exchanger 就是专门解决这类问题的同步工具。本文从 Exchanger 的底层原理讲起,分析 exchange 方法的阻塞与唤醒机制,对比 SynchronousQueue 和手动加锁等替代方案的差异,并结合缓冲区交换、遗传算法配对等典型场景给出可直接运行的代码示例,同时提醒栅栏点死锁、超时控制、中断处理等容易踩坑的地方,帮助你在合适的业务里正确使用这个轻量级的线程数据交换同步点。

JDK 并发包里除了常见的锁和 CountDownLatch,还有一个相对低调的工具类 Exchanger。它解决的是一个很具体的问题:两个线程各自持有对象,在约定的同步点上互相交换,A 拿到 B 的数据,B 拿到 A 的数据。这个语义用锁和条件变量也能实现,但代码会繁琐不少。本文从使用方式、底层实现和适用场景三个角度,把 Exchanger 讲清楚。

Exchanger 变量交换:如何实现两个线程之间安全交换数据的同步点

Exchanger 的基本用法与核心语义

Exchanger 的 API 极其简单,核心只有一个 exchange(V x) 方法。线程调用该方法时把自己的数据作为参数传入,如果对方线程还没到达交换点,当前线程就阻塞等待;一旦两个线程都到达,JVM 内部就把两者的数据互换并各自返回。也就是说,exchange 的返回值不是你传入的对象,而是对方线程传入的对象。

import java.util.concurrent.Exchanger;

public class ExchangerDemo {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();

        new Thread(() -> {
            try {
                String data = "生产者提交的数据";
                String received = exchanger.exchange(data);
                System.out.println("线程A 拿到了: " + received);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "线程A").start();

        new Thread(() -> {
            try {
                String data = "消费者回传的确认";
                String received = exchanger.exchange(data);
                System.out.println("线程B 拿到了: " + received);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "线程B").start();
    }
}

运行后你会发现,线程 A 打印的是线程 B 的数据,反之亦然。这里有几个值得注意的细节。第一,Exchanger 是泛型类,两边的类型必须一致,编译期就能约束住数据类型。第二,exchange 可能抛出 InterruptedException,因为等待方可能被中断唤醒,调用处必须处理好中断语义,通常做法是恢复中断标志。第三,如果担心对方迟迟不来导致自己永久阻塞,可以使用带超时的重载方法 exchange(V x, long timeout, TimeUnit unit),超时后抛出 TimeoutException 而不是无限等待。

另一个常被忽略的特性是:当参与交换的线程数量超过两个时,Exchanger 并不会报错,而是让线程两两自由配对,具体谁和谁交换取决于到达顺序和内部调度。JDK 文档把这个行为描述为可以当作双向同步栅栏使用。所以如果你的业务严格要求固定的两个线程交换,必须在逻辑上自己保证只有两个线程参与,否则结果可能不符合预期。

底层原理:exchange 如何完成阻塞与配对

Exchanger 在 JDK 8 之后的实现基于一个叫做 PairExchanger 的内部结构,核心是 Slot 和 Node 两个内部类,并利用了 @sun.misc.Contended 注解做伪共享消除。简单来说,每个 Exchanger 实例内部维护一个 slot 槽位,第一个到达的线程把自己的数据和线程引用包装成 Node 放进 slot,然后通过自旋加阻塞的方式等待;第二个到达的线程发现 slot 里已经有人,就取出对方的 Node,把自己的数据填进去,并唤醒第一个线程,双方各自返回对方的数据。

这里的性能设计很有讲究。单个 slot 的场景(也就是典型的两线程交换)走的是精心优化的快路径:先自旋若干次,避免线程刚到就被挂起,因为对方可能马上就到;自旋耗尽后才通过 LockSupport 挂起。而 slot 数组的设计是为了支持多个 Exchanger 调用方并发交换的场景,减少同一缓存行上的竞争。Contended 注解会把 slot 填充到独立的缓存行,避免不同 slot 的写入互相干扰对方的 CPU 缓存,这在高并发下对吞吐量的提升非常明显。

从内存语义上看,exchange 相当于一个完整的同步点:数据通过 Node 中的引用传递,配合 volatile 写和 CAS 操作保证可见性,一方对数据的修改在另一方 exchange 返回之后一定可见。这意味着你不需要额外加 volatile 或锁来保护被交换的对象本身,但要注意交换的是引用,如果交换后双方仍然并发修改同一个可变对象,依旧会有线程安全问题。安全的做法是交换不可变对象,或者交换后各自只操作自己拿到的那个对象。

典型应用场景与替代方案对比

Exchanger 最经典的场景是流水线式的缓冲区交换:一个线程负责填充缓冲区(比如从网络或磁盘读数据),另一个线程负责消费缓冲区(解析、写库),两个缓冲区轮流交换,填充和消费永不互相干扰。这在高性能 IO 处理中能显著减少内存分配和 GC 压力。

import java.util.concurrent.Exchanger;

public class BufferSwapDemo {
    static final int CAPACITY = 1024;

    public static void main(String[] args) {
        Exchanger<char[]> exchanger = new Exchanger<>();

        new Thread(() -> {
            char[] buffer = new char[CAPACITY];
            try {
                while (true) {
                    // 填充数据,模拟读取
                    for (int i = 0; i < CAPACITY; i++) {
                        buffer[i] = (char) ('A' + (i % 26));
                    }
                    buffer = exchanger.exchange(buffer); // 交出满缓冲区,拿回空缓冲区
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "填充线程").start();

        new Thread(() -> {
            char[] buffer = new char[CAPACITY];
            try {
                while (true) {
                    buffer = exchanger.exchange(buffer); // 交出空缓冲区,拿回满缓冲区
                    // 消费数据,模拟处理
                    System.out.println("处理到缓冲区首字符: " + buffer[0]);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "消费线程").start();
    }
}

这个例子的精妙之处在于缓冲区对象在两个线程之间循环流转,全程零分配。类似思路也可以用在遗传算法中个体配对交叉、校对任务双线程互相比对结果等场合。

那么什么时候不该用 Exchanger?如果交换不是成对发生的,或者参与方超过两个,应该考虑 SynchronousQueue 或者 BlockingQueue。SynchronousQueue 支持多生产者多消费者,但它是单向传递,数据只从一方流向另一方,不返回值;而 Exchanger 是双向的,一次调用完成交换,语义更贴合配对场景。如果只是想传递单向数据,用队列更合适。至于用共享变量加锁自己实现交换,虽然可行,但要处理状态标志、条件等待、唤醒顺序等一堆细节,容易写出死锁或丢失唤醒的 bug,除非有特殊需求,否则没必要重复造轮子。

总结一下,Exchanger 是一个语义清晰、实现精巧的轻量级同步点,适合两个线程在固定节奏下互换数据的场景。使用时把握三点:确保恰好两个线程参与、正确处理中断和超时、交换不可变对象或交换后各自独占修改,就能安全又高效地把它用起来。

Exchanger线程同步并发编程修改时间:2026-09-08 12:38:03

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260908/52770.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。