并发王者课-铂金9:互通有无-Exchanger如何完成线程间的数据交换
欢迎来到《并发王者课》,本文是该系列文章中的第22篇,铂金中的第9篇。
在前面的文章中,我们已经介绍了ReentrantLock,CountDownLatch,CyclicBarrier,Semaphore等同步工具。在本文中,将为你介绍最后一个同步工具,即Exchanger.
Exchanger用于两个线程在某个节点时进行数据交换。在用法上,Exchanger并不复杂,但是实现上会稍微有点费解。所以,考虑到Exchanger在平时使用的场景并不多,况且多数读者对一些“枯燥”的源码的耐受度有限(可能引起不适或烦躁等不良情绪,阻碍学习),本文将侧重讲它的使用和思想,对于源码不会过多展开,点到为止。
一、Exchanger的使用场景
在峡谷中,铠和兰陵王都是擅长打野的英雄,各自对野怪的偏好也不完全相同。所以,为了能得到自己想要的野怪,他们经常会在峡谷的交易中心交换各自的猎物。
这一天,铠打到了一只棕熊,而兰陵王则收获了一只野狼,并且彼此都想要对方的野怪。于是,他们约定在峡谷交易中心交换双方的野怪,谁先到了就先等会。这个过程,可以用下面这幅图来表示:
在铠和兰陵王交换猎物的过程中,有三个点需要你留意:
- 交换的双方有明确的交易地点(峡谷交易中心);
- 交换的双方具有明确的交易对象(比如棕熊和野狼);
- 谁先到了就等会儿(他们中总会有先来后到)。
如果用代码来实现的话,也是有多种方式可以选择,比如前面所学过的同步方法等。不过,虽然做也是可以做的,只是没那么方便。所以,接下来我们就用Exchanger来实现这一过程。
在下面的代码中,我们定义了一个exchanger
,它就类似于峡谷交易中心,而它的类型Exchanger<WildMonster>
则明确表示交换的对象是野怪。
接着,我们再定义两个线程,分别代表铠和兰陵王。在其线程的内部,会通过前面定义的exchanger
对象来和对方进行交换数据。交换完成后,他们彼此将获得对方的物品。
public static void main(String[] args) {
Exchanger<WildMonster> exchanger = new Exchanger<> (); // 定义交换地点和交换类型
Thread 铠 = newThread("铠", () -> {
try {
WildMonster wildMonster = new Bear("棕熊");
say("我手里有一只:" + wildMonster.getName());
WildMonster exchanged = exchanger.exchange(wildMonster); // 交换后将获得对方的物品
say("交易完成,我获得了:", wildMonster.getName(), "->", exchanged.getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread 兰陵王 = newThread("兰陵王", () -> {
try {
WildMonster wildMonster = new Wolf("野狼");
say("我手里有一只:" + wildMonster.getName());
WildMonster exchanged = exchanger.exchange(wildMonster);
say("交易完成,我获得了:", wildMonster.getName(), "->", exchanged.getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
铠.start();
兰陵王.start();
}
下面是上面代码用到的内部类:
@Data
private static class WildMonster {
protected String name;
}
private static class Wolf extends WildMonster {
public Wolf(String name) {
this.name = name;
}
}
private static class Bear extends WildMonster {
public Bear(String name) {
this.name = name;
}
}
示例代码运行结果如下:
铠:我手里有一只:棕熊
兰陵王:我手里有一只:野狼
兰陵王:交易完成,我获得了:野狼->棕熊
铠:交易完成,我获得了:棕熊->野狼
Process finished with exit code 0
从结果中可以看到,铠用棕熊换到了野狼,而兰陵王则用野狼换到了棕熊,他们完成了交换。
以上就是Exchanger的用法,看起来还是非常简单的,事实上也确实很简单。在使用Exchanger的时候要注意下面几点:
- 定义Exchanger对象,各线程通过这个对象完成交换;
- 在Exchanger对象中要定义类型,也就是这两个线程要交换什么;
- 线程在调用Exchanger进行交换时,要特别注意的是,先到的那个线程会原地等待另外一个线程的出现。比如,铠先到交换地点,可这时候兰陵王还没有到,那么铠会等待兰陵王的出现,除非超过设置的时间限制,比如兰陵王中途被妲己蹲了草丛。反之亦然,兰陵王先到也到等铠的出现。
二、Exchanger的源码与实现
虽然理解Exchanger的思想很容易,了解其用法也很简单,但是若要理清它几百余行的源码却并非易事。其原因在于,槽是Exchanger中的核心概念和属性,Exchanger中的数据交换分为单槽交换和多槽交换,其中单槽交换源码简单,但多槽交换却很复杂。所以,下文对Exchanger源码的阐述以概括为主,不会对源码深究。如果你有兴趣,可以参考阅读这篇文章,作者对其源码的解读较为详细。
1. 核心构造
与其他同步工具不同的是,Exchanger有且仅有一个构造函数。在这个构造中,也只初始化了一个对象participant
.
public Exchanger() {
participant = new Participant();
}
从继承关系看,Participant本质上是一个ThreadLocal,而其中的Node则是线程的本地变量。
static final class Participant extends ThreadLocal<Node> {
public Node initialValue() {
return new Node();
}
}
2. 核心属性
Exchanger有四个核心变量,如下所示。当然,除此之外,还有一些用以计算的其他变量。不过,为避免引入不必要的复杂度,本文暂不提及。
//ThreadLocal变量,每个线程都有自己的一个副本
private final Participant participant;
//多槽位,高并发下使用,保存待匹配的Node实例
private volatile Node[] arena;
//单槽位,arena未初始化时使用的保存待匹配的Node实例
private volatile Node slot;
//初始值为0,当创建arena后会被赋值成SEQ,用来记录arena数组的可用最大索引,会随着并发的增大而增大直到等于最大值FULL,会随着并行的线程逐一匹配成功而减少恢复成初始值
private volatile int bound;
Node的具体细节,注意其中的item和match.
@sun.misc.Contended static final class Node {
int index; //arena的下标,多个槽位的时候使用
int bound; // 上一次记录的Exchanger.bound
int collides; // 记录的 CAS 失败数
int hash; // 用于自旋
Object item; // 这个线程的数据项
volatile Object match; // 交换的数据
volatile Thread parked; // 当阻塞时,设置此线程,不阻塞的话会自旋
}
3. 核心方法
// 交换数据
// 如果一个线程达到后,会等待其他线程的到达(除非自己被中断)。然后,该线程会和到达的线程交换数据。
// 如果线程在到达后,已经有其他线程在等待。那么,将会唤起该线程并交换数据。
public V exchange(V x) throws InterruptedException {...}
//带有超时限制的交换
public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {...}
所以,从源码上看上文的示例,那么铠和兰陵王交换数据的过程应该是下面这样的:
小结
以上就是关于Exchanger的全部内容。在学习Exchanger时,要侧重理解它所要解决的问题场景,以及它的基本用法。对于其源码,当前阶段可以选择“不求甚解”,以降维的方式降低学习难度,日后再循序渐进理解。我在写本文时,也曾多次考虑是否要讲清楚源码,最终还是决定暂缓,毕竟现阶段理解它、学会它才是重点。
正文到此结束,恭喜你又上了一颗星
夫子的试炼
- 使用Exchanger实现生产者与消费者。
延伸阅读与参考资料
关于作者
关注【技术八点半】,及时获取文章更新。传递有品质的技术文章,记录平凡人的成长故事,偶尔也聊聊生活和理想。早晨8:30推送作者品质原创,晚上20:30推送行业深度好文。
如果本文对你有帮助,欢迎点赞、关注、监督,我们一起从青铜到王者。
并发王者课-铂金9:互通有无-Exchanger如何完成线程间的数据交换的更多相关文章
- 并发王者课-铂金1:探本溯源-为何说Lock接口是Java中锁的基础
欢迎来到<并发王者课>,本文是该系列文章中的第14篇. 在黄金系列中,我们介绍了并发中一些问题,比如死锁.活锁.线程饥饿等问题.在并发编程中,这些问题无疑都是需要解决的.所以,在铂金系列文 ...
- 并发王者课-铂金6:青出于蓝-Condition如何把等待与通知玩出新花样
欢迎来到<[并发王者课](https://juejin.cn/post/6967277362455150628)>,本文是该系列文章中的**第19篇**. 在上一篇文章中,我们介绍了阻塞队 ...
- 并发王者课-铂金8:峡谷幽会-看CyclicBarrier如何跨越重峦叠嶂
欢迎来到<并发王者课>,本文是该系列文章中的第21篇,铂金中的第8篇. 在上一篇文章中,我们介绍了CountDownLatch的用法.在协调多线程的开始和结束时,CountDownLatc ...
- 并发王者课-铂金10:能工巧匠-ThreadLocal如何为线程打造私有数据空间
欢迎来到<并发王者课>,本文是该系列文章中的第23篇,铂金中的第10篇. 说起ThreadLocal,相信你对它的名字一定不陌生.在并发编程中,它有着较高的出场率,并且也是面试中的高频面试 ...
- 并发王者课-铂金2:豁然开朗-“晦涩难懂”的ReadWriteLock竟如此妙不可言
欢迎来到<并发王者课>,本文是该系列文章中的第15篇. 在上篇文章中,我们介绍了Java中锁的基础Lock接口.在本文中,我们将介绍Java中锁的另外一个重要的基本型接口,即ReadWri ...
- [笔记][Java7并发编程实战手冊]3.8 并发任务间的数据交换Exchanger
[笔记][Java7并发编程实战手冊]系列文件夹 简单介绍 Exchanger 是一个同步辅助类.用于两个并发线程之间在一个同步点进行数据交换. 同意两个线程在某一个点进行数据交换. 本章exchan ...
- 并发王者课 - 青铜4:synchronized用法初体验
在前面的文章<双刃剑-理解多线程带来的安全问题>中,我们提到了多线程情况下存在的线程安全问题.本文将以这个问题为背景,介绍如何通过使用synchronized关键字解这一问题.当然,在青铜 ...
- 并发王者课 - 青铜 2:峡谷笔记 - 简单认识Java中的线程
在前面的<兵分三路:如何创建多线程>文章中,我们已经通过Thread和Runnable直观地了解如何在Java中创建一个线程,相信你已经有了一定的体感.在本篇文章中,我们将基于前面的示例代 ...
- 并发王者课-青铜5:一探究竟-如何从synchronized理解Java对象头中的锁
在前面的文章<青铜4:synchronized用法初体验>中,我们已经提到锁的概念,并指出synchronized是锁机制的一种实现.可是,这么说未免太过抽象,你可能无法直观地理解锁究竟是 ...
随机推荐
- Rsync忽略文件夹或目录
使用Rsync同步的时候往往会要求对某个文件夹或者文件进行忽略,客户端可以使用--exclude参数来实现对,目录或者文件的忽略 rsync -rltvz --port=873 --exclude & ...
- 8.模块定义导入优化time datetime内置模块
1.模块(module)的定义:本质就是.py的python文件用来从逻辑上组织python代码(变量\函数\类\逻辑:实现一个功能)包(package)的定义:用来从逻辑上组织模块的,本质就是一个文 ...
- USB中TOKEN的CRC5与CRC16校验(神奇的工具生成Verilog实现)
USB2.0IP设计 最近,在学习USB2.0IP的设计,其中包含了CRC校验码的内容,之前学习千兆以太网曾经用到过CRC32校验(https://www.cnblogs.com/Xwangzi66/ ...
- 『言善信』Fiddler工具 — 1、Fiddler介绍与安装
目录 1.Fiddler简介 2.Fiddler功能 3.Fiddler工作原理 (1)先来了解一下B/S架构 (2)Fiddler工作原理 (3)Fiddler工作原理进阶说明 (4)以Google ...
- VMware Tanzu Kubernetes Grid 1.3 发布 - VMware 构建、签名和支持的开源 Kubernetes 容器编排平台的完整分发版
Tanzu Kubernetes 集群是由 VMware 构建.签名和支持的开源 Kubernetes 容器编排平台的完整分发版.可以通过使用 Tanzu Kubernetes Grid 服务在主管集 ...
- NVIDIA 自动驾驶软件平台
NVIDIA 自动驾驶软件平台 Software Developers using DRIVE AGX Developer Kits may choose between: DRIVE OS 5.2. ...
- MySQL笔记01(黑马)
一.数据库基本介绍 目标:了解数据库的功能和常见数据库分类.数据库产品 数据库基本知识 数据库分类 SQL简介 MySQL访问 1.数据库基本知识 目标:了解数据库的概念和数据库的作用 概念 数据库: ...
- 【Azure 应用服务】Azure Web App的服务(基于Windows 操作系统部署)在被安全漏洞扫描时发现了TCP timestamps漏洞
问题背景 什么是TCP timestamps(TCP 时间戳)? The remote host implements TCP Timestamps, as defined by RFC1323 (h ...
- 使用有道云笔记还是github写笔记的优缺点对比
有道云笔记的优点 在手机上编辑笔记,有道云的体验更好,建议:新建普通类型笔记而不是markdown笔记,因为md在手机上的编辑体验并不好 插入图片方便无需考虑图床 可以把笔记分享到社交平台(QQ,微信 ...
- 面试官:给我讲讲SpringBoot的依赖管理和自动配置?
1.前言 从Spring转到SpringBoot的xdm应该都有这个感受,以前整合Spring + MyBatis + SpringMVC我们需要写一大堆的配置文件,堪称配置文件地狱,我们还要在pom ...