文章目录
Java并发编程中的Exchanger:
1、定义: Exchanger是一个用于线程间协作的工具类,用于进行线程间的数据交换。
2、用途: 它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。
📢📢 对账系统死锁后,Exchanger 教我给两个线程 “精准交换数据”!

零、引入
你刚接手公司电商的 “订单 - 支付对账” 功能,拍胸脯跟领导说 “3 天搞定,线程通信稳如老狗”。结果上线第一天,对账数据错得像被猫踩过的 Excel—— 订单线程传的 “100 笔订单”,支付线程只收到 80 笔,还触发了死锁,服务器 CPU 飙到 90%,财务小姐姐拿着对账单追到你工位:“少算的 20 笔账,你是想自己垫还是扣绩效?”
更要命的是,你用的是 “共享队列 + 锁” 的老办法:订单线程往队列塞数据,支付线程从队列取,加了一堆 synchronized 和 wait/notify,结果要么锁没释放导致死锁,要么数据没同步导致漏传。领导把你叫到办公室,杯子往桌上一墩:“3 小时内修复,不然这个月全勤奖、绩效奖全扣!”
就在你对着代码抓头发,甚至想把 “HashMap” 改成 “Hashtable” 碰碰运气时,隔壁王哥叼着刚买的肉夹馍凑过来:“慌啥?两个线程交换数据,用 Exchanger 啊!这玩意儿就是线程界的‘快递交换站’,比你这堆锁靠谱 10 倍!”
一、先吐槽你的 “笨办法”:共享变量 + 锁 = 自找苦吃
王哥扒拉了两口肉夹馍,指着你的代码笑出了抬头纹:“你看你这逻辑,订单线程和支付线程抢同一个队列,就像两个快递员抢同一个快递柜,一个塞一个取,还都抱着锁不放 —— 不死锁才怪!更离谱的是,你还得手动判断‘数据传完没’,就像快递员塞完包裹,得打电话喊对方来取,漏接电话就丢件,这不就是你数据少 20 笔的原因?”
1.1 你的错误代码(共享队列版,易死锁、易丢数据)
import java.util.LinkedList;
import java.util.Queue;
public class BadReconciliationDemo {
// 共享队列:订单线程塞数据,支付线程取数据
static Queue<String> dataQueue = new LinkedList<>();
// 锁对象
static final Object LOCK = new Object();
// 标记:订单数据是否传完
static boolean orderDataReady = false;
public static void main(String[] args) {
// 订单线程:模拟读取100笔订单数据
Thread orderThread = new Thread(() -> {
synchronized (LOCK) {
for (int i = 1; i <= 100; i++) {
dataQueue.add("订单" + i);
}
orderDataReady = true;
System.out.println("订单线程:100笔订单已塞到队列,喊支付线程来取!");
LOCK.notify(); // 喊支付线程取数据
}
}, "订单线程");
// 支付线程:模拟读取100笔支付数据,然后取订单数据对账
Thread payThread = new Thread(() -> {
synchronized (LOCK) {
// 先等订单数据传完
while (!orderDataReady) {
try {
System.out.println("支付线程:等订单数据...");
LOCK.wait(); // 死等,要是notify漏了就一直等
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 取订单数据(这里大概率漏数据,因为队列遍历可能被打断)
int count = 0;
while (!dataQueue.isEmpty()) {
dataQueue.poll();
count++;
}
System.out.println("支付线程:只拿到" + count + "笔订单数据(实际该100笔)");
}
}, "支付线程");
// 启动线程(顺序错了也会出问题)
payThread.start();
orderThread.start();
}
}
跑起来大概率输出:支付线程:只拿到98笔订单数据(实际该100笔)—— 要么漏数据,要么偶尔死锁,气得你想砸键盘。
插个冷笑话:“你这代码,就像我上学时和同桌传纸条,老师(JVM)盯着呢,我塞纸条(塞数据)快了,同桌没接住(没取到),纸条掉地上(数据丢了);我塞慢了,老师过来抓现行(死锁)—— 纯纯的反面教材!”
二、Exchanger:两个线程的 “精准交换站”

王哥把肉夹馍放一边,打开 IDE:“Exchanger 是 JUC 里专门给两个线程 交换数据的工具,核心就一个逻辑:两个线程都跑到‘交换点’(调用 exchange 方法),才能交换数据;只要有一个没到,另一个就等着,直到对方来,或者超时。”
2.1 先搞懂 Exchanger 的核心规则(记牢!)
- 专属 “双人交换”:只能两个线程交换,多了没用 —— 就像快递交换站只接两个快递员的包裹,第三个来的只能干等;
- 交换才放行:线程 A 调用exchange(dataA),会阻塞直到线程 B 调用exchange(dataB),然后 A 拿到 dataB,B 拿到 dataA,俩人一起放行;
- 支持超时:怕等太久?用exchange(data, 5, TimeUnit.SECONDS),5 秒没等到对方,直接抛超时异常,不会死等;
- 数据类型一致:交换的得是同类型数据(比如都是 List),不然编译都过不了 —— 总不能拿 “订单数据” 换 “红烧肉” 吧?
2.2 生活场景比喻

“比如你和快递员小哥约在小区门口交换包裹:你拿他的快递(支付数据),他拿你的退货包裹(订单数据)。你到了,小哥没到,你就得等;小哥到了,你俩交换完,各自走人 —— 这就是 Exchanger 的逻辑,简单到离谱!”
修复后的代码(Exchanger 版,精准交换不丢数据)
package cn.tcmeta.exchangers;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Exchanger;
/**
* @author: laoren
* @description: Exchanger 版,精准交换不丢数据
* @version: 1.0.0
*/
public class GoodReconciliationSample {
// 定义Exchanger,泛型指定交换的数据类型:List<String>
static final Exchanger<List<String>> DATA_EXCHANGER = new Exchanger<>();
static void main() {
// 线程1:读取订单数据(模拟100笔)
Thread orderThread = new Thread(() -> {
List<String> orderList = new ArrayList<>();
// 模拟读取100笔订单数据
for (int i = 1; i <= 100; i++) {
orderList.add("订单" + i);
}
System.out.println("订单线程:100笔订单数据已准备好,去交换站等支付线程!");
try {
// 到交换站,拿出订单数据,等待支付线程交换
// 交换后,orderThread会拿到支付线程的支付数据
List<String> payList = DATA_EXCHANGER.exchange(orderList);
// 对账逻辑:对比订单数和支付数
System.out.println("订单线程:拿到支付数据共" + payList.size() + "笔");
if (orderList.size() == payList.size()) {
System.out.println("对账结果:订单数=支付数,没差账!");
} else {
System.out.println("对账结果:差账了!订单" + orderList.size() + "笔,支付" + payList.size() + "笔");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "订单线程");
// 线程2:读取支付数据(模拟100笔)
Thread payThread = new Thread(() -> {
List<String> payList = new ArrayList<>();
// 模拟读取100笔支付数据
for (int i = 1; i <= 100; i++) {
payList.add("支付" + i);
}
System.out.println("支付线程:100笔支付数据已准备好,去交换站等订单线程!");
try {
// 到交换站,拿出支付数据,等待订单线程交换
// 交换后,payThread会拿到订单线程的订单数据
List<String> orderList = DATA_EXCHANGER.exchange(payList);
System.out.println("支付线程:拿到订单数据共" + orderList.size() + "笔");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "支付线程");
// 启动线程(顺序随便,Exchanger会等对方)
orderThread.start();
payThread.start();
}
}

“你看,不用锁、不用队列、不用手动通知,两行 exchange () 就搞定数据交换!” 王哥拍桌,“Exchanger 帮你把‘等对方、交换数据、释放线程’全做了,比你那堆锁省心 10 倍!”
三、Exchanger 的超时版:怕等太久?给个时间限制!

“要是其中一个线程挂了,另一个总不能死等吧?” 你突然想到问题,王哥立马补了个超时版代码 —— 这才是线上环境的正确用法,避免线程一直阻塞。
超时版代码(避免死等)
package cn.tcmeta.exchangers;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @author: laoren
* @description: 避免死等
* @version: 1.0.0
*/
public class ExchangerTimeoutSample {
static final Exchanger<List<String>> DATA_EXCHANGER = new Exchanger<>();
static void main() {
// 订单线程:正常准备数据
Thread orderThread = new Thread(() -> {
List<String> orderList = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
orderList.add("订单" + i);
}
System.out.println("订单线程:准备好数据,等支付线程(最多等5秒)");
try {
// 最多等5秒,5秒没等到就抛超时异常
List<String> payList = DATA_EXCHANGER.exchange(orderList, 5, TimeUnit.SECONDS);
System.out.println("订单线程:交换成功,拿到" + payList.size() + "笔支付数据");
} catch (InterruptedException e) {
System.out.println("订单线程:等待时被打断");
} catch (TimeoutException e) {
System.out.println("订单线程:等了5秒,支付线程还没来,不等了!");
}
}, "订单线程");
// 支付线程:模拟卡死(比如读取数据时抛异常)
Thread payThread = new Thread(() -> {
System.out.println("支付线程:读取数据时卡死了...");
// 故意不调用exchange,模拟线程异常
try {
Thread.sleep(10000); // 睡10秒,超过订单线程的5秒等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "支付线程");
orderThread.start();
payThread.start();
}
}

“线上环境必须加超时!” 王哥强调,“不然一个线程挂了,另一个线程能等成化石 —— 上次小李没加超时,一个线程卡死,另一个线程等了 3 小时,导致对账系统全天瘫痪,被领导罚抄了 10 遍 Exchanger 源码!”
四、Exchanger 的核心使用场景(看完就能用)

王哥擦了擦嘴,给你列了 3 个最常用的场景,每个都能直接套代码:
- 场景 1:双线程数据对账(你的核心需求)
比如订单 - 支付对账、库存 - 销售对账、流水 - 发票对账 —— 两个线程分别读取两类数据,交换后对比,比共享队列靠谱 100%。 - 场景 2:游戏中的玩家道具交换
比如王者荣耀里两个玩家交换皮肤:玩家 A 线程拿 “史诗皮肤”,玩家 B 线程拿 “限定皮肤”,调用 exchange 交换,交换成功才完成交易,避免一方给了道具另一方没给。 - 场景 3:数据采集与处理分离
比如一个线程采集爬虫数据(网页内容),一个线程处理数据(解析、入库),采集完调用 exchange 把数据传给处理线程,处理线程拿到数据后,再把 “处理完成的状态” 交换回去 —— 分工明确,不扯皮。
插个脱口秀段子:“Exchanger 就像程序员之间交换 bug:你写了个空指针,我写了个数组越界,咱俩都到工位(调用 exchange),才能交换 bug;你到了我没到,你就得等我摸完鱼 —— 这就是职场版的 Exchanger,主打一个‘互相拖累,绝不独行’!”
五、Exchanger 的坑:别踩!

王哥给你划了 3 个坑,踩一个就加班:
- 别给多线程用:Exchanger 只支持两个线程交换,三个线程调用 exchange,会随机两两交换,剩下一个死等 —— 就像三个快递员交换包裹,俩换完走了,剩下一个抱着包裹等半天,贼尴尬;
- 交换数据要非空:别传 null 进去交换,虽然编译过得了,但线上容易空指针 —— 总不能拿 “空气” 换 “订单数据” 吧?
- 超时时间要合理:太短容易误判(比如对方只是慢了点),太长等于没加 —— 比如对账场景,设 5 秒够了,设 1 小时和死等没区别。
彩蛋:王哥的血泪史
“我刚用 Exchanger 时,手滑写了两个 Exchanger 对象,” 王哥捂脸,“订单线程用 Exchanger1,支付线程用 Exchanger2,结果俩线程各等各的,我以为系统卡了,重启了 3 次服务器,最后发现是自己建错了对象 —— 被领导骂‘连交换站都建错,不如去干快递’!”
六、总结:3 分钟上手的核心口诀

王哥拍了拍你的肩膀,给你总结了 “傻瓜式口诀”,记牢就能直接用:
- 双线程交换数据:优先用 Exchanger,别用共享变量 + 锁;
- 核心方法 exchange:传自己的数 - 据,等对方传数据,交换后拿到对方的;
- 线上必加超时:exchange (数据,超时时间,单位),避免死等;
- 只给俩线程用:多线程交换别找它,找 BlockingQueue 更合适。
七、最后说句实在的

-Exchanger 不是啥高大上的技术,就是专门解决 “两个线程交换数据” 的小工具 —— 场景对口,用起来比 wait/notify、Lock/Condition 简洁 10 倍;场景不对(多线程),别硬用。
今天这 3 段代码你复制过去就能跑,改改泛型和数据内容,就能用到对账、游戏道具交换、数据采集等场景里,再也不用写一堆锁和通知逻辑。
要是你搞懂了,别光顾着自己爽,点赞让更多人避坑,关注我 —— 下次咱们扒一扒 “Phaser 怎么搞定动态多线程同步”,这玩意儿比 CountDownLatch、CyclicBarrier 还灵活,能让 N 个线程按阶段同步,彻底告别 “线程协作加班” 的噩梦!
对了,把这篇分享给你那用 “共享队列 + 锁” 处理双线程数据交换的同事,下次代码 review 时,你就能笑着说:“兄弟,别让线程抢快递柜了,给它们建个专属交换站(Exchanger),交换数据比摸鱼还轻松!”




Exchanger实现线程精准数据交换
747

被折叠的 条评论
为什么被折叠?



