一、 前言

在一个风黑月高的夜晚,程序员小强与项目经理小月鸟商定,在南京路旁边小水沟里进行一场交易,交易的物品是小强完成的项目和项目经理给小强的项目奖金。

在南京路将会有一场腥风血雨产生。

二、 平稳交易开始

import java.util.concurrent.Exchanger;

/**
* @author 发现更多精彩 关注公众号:木子的昼夜编程
* 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
*/
public class Test {
public static void main(String[] args) {
Exchanger ex = new Exchanger();
new Thread(()->{
try {
System.out.println("小强来到南京路小水沟,准备好项目。");
Object obj = ex.exchange("项目");
System.out.println("小强收到了:"+obj+",抓紧离去");
} catch (Exception e) {
System.out.println("有内鬼终止交易");
}
}).start();

new Thread(()->{
try {
// 项目经理觉得自己高人一等 非要先等会儿再到
System.out.println("小月鸟先等会儿");
Thread.sleep(5000);
System.out.println("小月鸟来到南京路小水沟,准备好钱。");
Object obj = ex.exchange("奖金");
System.out.println("小月鸟收到了:"+obj+",抓紧离去");
} catch (Exception e) {
System.out.println("有内鬼终止交易");
}
}).start();
}
}

输出结果:

无间道——程序员版_项目经理

三、暴躁的交易

话说,交易这天小强心情不好,因为早上因为骑电车没带头盔被罚了款,所以他决定这次不惯着项目经理,他到了如果项目经理没有到,最多等他三秒钟,过了三秒钟项目经理不到的话就直接走。

import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
* @author 发现更多精彩 关注公众号:木子的昼夜编程
* 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
*/
public class TestTime {
public static void main(String[] args) {
Exchanger ex = new Exchanger();
new Thread(()->{
try {
System.out.println("小强来了,准备好项目。就等项目经理三秒钟");
// 3秒 可指定单位
Object obj = ex.exchange("项目", 3, TimeUnit.SECONDS);
System.out.println("小强收到了:"+obj);
} catch (Exception e) {
e.printStackTrace();
System.out.println("小强收到消息:有内鬼终止交易");
if (e instanceof TimeoutException) {
System.out.println("其实小强是自己不想等,自己走了");
}

}
}).start();

new Thread(()->{
try {
// 项目经理比较好面 先等会儿再到
Thread.sleep(10000);
System.out.println("小月鸟来了,准备好钱。");
// 小强已经不来了,这时候项目经理在这里等到海枯石烂也等不来小强
// 所以我们用这个方法的时候,最好给一个超时时间,以防万一别的exchange出幺蛾子
// 哪怕给一个一年也行 不至于让人等到海枯石烂
Object obj = ex.exchange("奖金");
System.out.println("小月鸟收到了:"+obj);
} catch (Exception e) {
System.out.println("小月鸟收到消息:有内鬼终止交易");
}
}).start();
}
}

输出结果:

无间道——程序员版_项目经理_02

四、第三者

这天小明收到消息,知道小强和小月鸟要进行交易,他想伪装小强去跟小月鸟进行交易。

但是他们三个都没有打过照面,他们三个都拿着一样的箱子,交易的时候不能说话,直接交换箱子。

无间道——程序员版_java_03

这时候会有三种情况:

小明和小强换箱子、小明和小月鸟换箱子、小强和小月鸟换箱子

import java.sql.Time;
import java.util.Random;
import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;

/**
* @author 发现更多精彩 关注公众号:木子的昼夜编程
* 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
* @create 2021-09-22 21:05
*/
public class TestMore {
public static void main(String[] args) throws InterruptedException {
// 做十次模拟
for (int i = 0; i < 10; i++) {
System.out.println("----第"+i+"次------------------------------------------");
test(i);
Thread.sleep(5000);
}
}

public static void test(int i){
Random random = new Random ();
Exchanger ex = new Exchanger();
new Thread(()->{
try {
// 随机3秒以内
Thread.sleep(random.nextInt(3));
System.out.println("小强来了,准备好项目。");
Object obj = ex.exchange("项目", 3, TimeUnit.SECONDS);
System.out.println("小强收到了:"+obj);
} catch (Exception e) {
System.out.println("有内鬼终止交易");
}
}).start();

new Thread(()->{
try {
// 随机3秒以内
Thread.sleep(random.nextInt(3));
System.out.println("小明来了,准备好项目。");
Object obj = ex.exchange("假项目", 3, TimeUnit.SECONDS);
System.out.println("小明收到了:"+obj);
} catch (Exception e) {
System.out.println("有内鬼终止交易");
}
}).start();


new Thread(()->{
try {
// // 随机3秒以内
Thread.sleep(random.nextInt(3));
System.out.println("小月鸟来了,准备好钱。");
Object obj = ex.exchange("奖金", 3, TimeUnit.SECONDS);
System.out.println("小月鸟收到了:"+obj);
} catch (Exception e) {
System.out.println("有内鬼终止交易");
}
}).start();
}
}

输出:

----第0次------------------------------------------
小月鸟来了,准备好钱。
小强来了,准备好项目。
小明来了,准备好项目。
小强收到了:奖金
小月鸟收到了:项目
有内鬼终止交易
----第1次------------------------------------------
小明来了,准备好项目。
小月鸟来了,准备好钱。
小强来了,准备好项目。
小月鸟收到了:假项目
小明收到了:奖金
有内鬼终止交易
----第2次------------------------------------------
小强来了,准备好项目。
小明来了,准备好项目。
小明收到了:项目
小月鸟来了,准备好钱。
小强收到了:假项目
有内鬼终止交易
----第3次------------------------------------------
小强来了,准备好项目。
小月鸟来了,准备好钱。
小月鸟收到了:项目
小强收到了:奖金
小明来了,准备好项目。
有内鬼终止交易
----第4次------------------------------------------
小月鸟来了,准备好钱。
小明来了,准备好项目。
小明收到了:奖金
小月鸟收到了:假项目
小强来了,准备好项目。
有内鬼终止交易
----第5次------------------------------------------
小强来了,准备好项目。
小月鸟来了,准备好钱。
小明来了,准备好项目。
小明收到了:项目
小强收到了:假项目
有内鬼终止交易
----第6次------------------------------------------
小明来了,准备好项目。
小月鸟来了,准备好钱。
小月鸟收到了:假项目
小明收到了:奖金
小强来了,准备好项目。
有内鬼终止交易
----第7次------------------------------------------
小明来了,准备好项目。
小月鸟来了,准备好钱。
小月鸟收到了:假项目
小明收到了:奖金
小强来了,准备好项目。
有内鬼终止交易
----第8次------------------------------------------
小明来了,准备好项目。
小月鸟来了,准备好钱。
小月鸟收到了:假项目
小强来了,准备好项目。
小明收到了:奖金
有内鬼终止交易
----第9次------------------------------------------
小强来了,准备好项目。
小月鸟来了,准备好钱。
小强收到了:奖金
小月鸟收到了:项目
小明来了,准备好项目。
有内鬼终止交易

Process finished with exit code 0

可以看到,结果是不确定的,那两个人先去就会进行交易,不会有主次之分,我们再用Exchanger的时候也需要注意,是否有多个线程使用,这时候交换的数据可能会乱套。

五、真有内鬼

exchange是可以被线程中断打断的

import java.util.concurrent.Exchanger;

/**
* @author 发现更多精彩 关注公众号:木子的昼夜编程
* 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
*/
public class TestNG {
public static void main(String[] args) {
Exchanger ex = new Exchanger();
Thread thread = new Thread(() -> {
try {
System.out.println("小强来到南京路小水沟,准备好项目。");
Object obj = ex.exchange("项目");
System.out.println("小强收到了:" + obj + ",抓紧离去");
} catch (Exception e) {
System.out.println("有内鬼终止交易");
if (e instanceof InterruptedException) {
System.out.println("exchange是可以被线程中断打断的");
}
}
});
thread.start();

new Thread(()->{
try {
// 项目经理觉得自己高人一等 非要先等会儿再到
System.out.println("小月鸟先等会儿");
Thread.sleep(10000);
System.out.println("小月鸟来到南京路小水沟,准备好钱。");
Object obj = ex.exchange("奖金");
System.out.println("小月鸟收到了:"+obj+",抓紧离去");
} catch (Exception e) {
System.out.println("有内鬼终止交易");
}
}).start();

// 滴滴滴 有内鬼终止交易
thread.interrupt();

}
}

无间道——程序员版_数据_04

六、交换多个物品呢

支持泛型

import java.util.concurrent.Exchanger;

/**
* @author 发现更多精彩 关注公众号:木子的昼夜编程
* 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
*/
public class TestV {
public static void main(String[] args) {
Exchanger ex = new Exchanger<TT>();
new Thread(()->{
try {
System.out.println("小强来到南京路小水沟,准备好项目。");
TT tt = new TT();
tt.name="项目";
tt.desc="真的很难写";
TT obj = (TT) ex.exchange(tt);
System.out.println("小强收到了:"+tt.name+","+tt.desc+",抓紧离去");
} catch (Exception e) {
System.out.println("有内鬼终止交易");
}
}).start();

new Thread(()->{
try {
// 项目经理觉得自己高人一等 非要先等会儿再到
System.out.println("小月鸟先等会儿");
Thread.sleep(5000);
System.out.println("小月鸟来到南京路小水沟,准备好钱。");
TT tt = new TT();
tt.name="奖金";
tt.desc="真的很心疼";
TT obj = (TT) ex.exchange(tt);
System.out.println("小月鸟收到了:"+tt.name+","+tt.desc+",抓紧离去");
} catch (Exception e) {
System.out.println("有内鬼终止交易");
}
}).start();
}
}

class TT{
// 名称
String name;
// 描述
String desc;
}

七、唠唠

他是怎么实现俩线程之间进行数据交换的呢。

用到了ThreadLocal、park/unpark、CAS、还有一套完善高级牛逼的处理逻辑。

1. 构造方法
public Exchanger() {
// 初始化一个Participant对象
participant = new Participant();
}
// Participant继承ThreadLocal 实现了initialValue
// ThreadLocal在get的时候如果没有值 会默认调用initialValue生成新对象
// ThreadLocal相关知识有时间我写一下(其实是学一下 哈哈)
static final class Participant extends ThreadLocal<Node> {
public Node initialValue() { return new Node(); }
}
2. exchange方法
// 如果要交换的数据为null 用NULL_ITEM代替 最后返回的时候会判断 如果是NULL_ITEM就返回null
private static final Object NULL_ITEM = new Object();
// 超时的话返回TIMED_OUT
private static final Object TIMED_OUT = new Object();

@SuppressWarnings("unchecked")
public V exchange(V x) throws InterruptedException {
// 接收返回值
Object v;
// 如果交换的数据为null 就用NULL_ITEM代替 // translate null args
Object item = (x == null) ? NULL_ITEM : x;
// 1. arena != null
// 2. v = slotExchange(item, false, 0L)) == null
// 3. Thread.interrupted()
// 4. v = arenaExchange(item, false, 0L)) == null)
// if (1 || 2) && (3 || 4) {抛出中断异常}
// 如果arena == null 就执行slotExchange(单槽操作)
// 否则执行arenaExchange(发生cas设置值失败时转换为多槽操作)
if ((arena != null ||
(v = slotExchange(item, false, 0L)) == null) &&
((Thread.interrupted() || // disambiguates null return
(v = arenaExchange(item, false, 0L)) == null)))
throw new InterruptedException();
// 如果v是自定义对象NULL_ITEM 也就是null占位符 那么直接返回null
// 否则返回v强转为V (泛型)
return (v == NULL_ITEM) ? null : (V)v;
}

什么是单槽操作,什么是多槽操作:

大家看到过电视剧里,有的交易是通过保险柜进行交易,A把货物放1号保险柜,B来1号保险柜拿走货物并把钱放到A保险柜,短信通知A,然后A来1号保险柜拿走钱。

但是如果上万人进行交易,那么一个保险柜可能同时会被多个人打开,这时候你也要打开,我也要打开,那就会打架了,所以这时候租了1000个保险柜,你来了,就从1号开始,挨个看,如果有东西可以拿走,你就把你的东西放进去,把东西拿走,然后短信通知上一个放东西的人(保险柜有纸条写着上一个人电话)

前者就是单槽操作,后者就是多槽操作。

多槽减少cas失败概率

无间道——程序员版_项目经理_05

3. slotExchange

我们看一下单槽操作的逻辑:

// 返回其他线程交换的item或者是线程被中断或者是超时(如果有超时)
private final Object slotExchange(Object item, boolean timed, long ns) {
// 获取当前线程专属Node (上边说过如果没有 ThreadLocal会调用initialValue生成一个)
Node p = participant.get();
// 获取当前线程对象(这里是为了把联系方式放到保险柜 等后边人来交换东西的时候进行通知 park unpark)
Thread t = Thread.currentThread();
// 如果线程是中断状态 直接返回null
if (t.isInterrupted())
return null;

//=============================
// 1. 看柜子里是否有货物 如果有那就拿出获取(cas),然后把自己获取放进去,通知上一个放货物的人
// 2. 如果有货物但是拿的时候有人跟自己抢 那就看看是否有钱(NCPU 也就是cpu数量是否大于1),
// 有的话就升级为多槽
//private static final int NCPU = Runtime.getRuntime().availableProcessors();
// 我之前文章提到过,for+cas是一个完美的组合 一般cas都会考虑失败的情况,
// 用死循环for能完美的解决这种情况
for (Node q;;) {
// 如果槽内有数据
if ((q = slot) != null) {
// cas获取保险柜货物(拿solt槽数据 并把solt槽设置为null)
// Node最主要的属性:
// 1. 当前线程交换的货物(数据) item
// 2. match 来跟当前线程交换的线程的货物(数据)
// 3. parked 当前线程
// 4. 这里的当前线程不是上下文线程的意思,是说这个Node对应的线程
// 可以理解成上一个来交换货物的人
if (U.compareAndSwapObject(this, SLOT, q, null)) {
// 如果拿数据成功了 获取交换数据 q.item
Object v = q.item;
// 把自己交换的数据放到q的match中
q.match = item;
// 获取联系方式 这里用到了park 和 unpark
// 这个之前文章也讲过,park是阻塞当前线程,直到线程被unpark
Thread w = q.parked;
if (w != null)
// 通知上一个人 拿货物
U.unpark(w);
return v;
}
// create arena on contention, but continue until slot null
if (NCPU > 1 && bound == 0 &&
U.compareAndSwapInt(this, BOUND, 0, SEQ))
arena = new Node[(FULL + 2) << ASHIFT];
}
else if (arena != null)
// 如果多槽 也就是cas失败了(或者是其他线程cas失败了)
// 返回null 会接着调用arenaExchange
return null;
else {
// 保险柜没有货物 把自己的货物放到保险柜(solt槽)里边
// cas方式存放 不然两个人放可能会一个覆盖另一个
p.item = item;
if (U.compareAndSwapObject(this, SLOT, null, p))
break;
p.item = null;
}
}
//=============================
// 货物放保险箱 等着其他人来交换货物
// 获取hash 默认是个0
int h = p.hash;
// 如果有超时 获取超时时间点 如果没有就是0
long end = timed ? System.nanoTime() + ns : 0L;
// 这是个骚操作,计算一个循环次数 我觉得是这样的,可能作者认为,用exchange的用户,
// 大概率都是在很短的时间内就要进行交换(跟我的实例一样),而不是等好久才进行交换,
// 所以这里先来了一拨乐观锁(循环)
// 这里会一直计算h 的值 如果h是负数 spins就-1
//直到spins减到SPINS的一半 就线程yield一下 让一下资源
//等获取到资源后接着循环 接着上边的循环
// 直到 p.match不为空,也就是循环过程中有人来交换货物
// 或者 spins-1 等于 0 了 就进行下边逻辑
int spins = (NCPU > 1) ? SPINS : 1;
Object v;
while ((v = p.match) == null) {
if (spins > 0) {
h ^= h << 1; h ^= h >>> 3; h ^= h << 10;
if (h == 0)
h = SPINS | (int)t.getId();
else if (h < 0 && (--spins & ((SPINS >>> 1) - 1)) == 0)
Thread.yield();
}
// 这个绝了 如果槽里的东西不是自己的p 也就是有人来交换货物了
// 再来一轮上边的乐观锁 spins设置为初始值 或走第一个if(spins > 0)
else if (slot != p)
spins = SPINS;
// 到这里才真正的安静下来
// 1. 线程没有中断 !t.isInterrupted()
// 2. 没有升级为多槽 arena != null
// 3. 没有超时 !timed || (ns = end - System.nanoTime()) > 0L)
// ( 1 && 2 ) 进行逻辑操作
else if (!t.isInterrupted() && arena == null &&
(!timed || (ns = end - System.nanoTime()) > 0L)) {
// 不是很理解这个作用
U.putObject(t, BLOCKER, this);
// 记录当前线程t (放电话号码到保险柜) 等别的线程来交换的时候 会通过parked来进行unpark操作(发短信提货)
p.parked = t;
// 如果当前槽是自己的货物 又一次判断槽里的货物是不是自己的 防止这个过程中有人进行了货物交换
if (slot == p)
// park终于安静了 等待别人来交换
// 什么时候交换呢 这个方法刚开始的时候 就是别的线程来交换逻辑
// 主要:1.拿设置solt为null 2.拿solt的item 3.设置p的match 4.unpark线程parked
U.park(false, ns);
// 被unprk之后 恢复参数parked BLOCKER 为null
p.parked = null;
U.putObject(t, BLOCKER, null);
}
else if (U.compareAndSwapObject(this, SLOT, p, null)) {
// 这个有3种情况 1. 线程中断了 2. 超时了 3.升级为多槽操作 需要调用arenaExchange
// 超时返回TIMED_OUT
// 为什么中断不返回特殊值呢 因为中断状态可以通过线程属性获取到 而超时不是个状态 是一个计算结果
v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null;
break;
}
}
// 恢复对应位置为初始值
U.putOrderedObject(p, MATCH, null);
p.item = null;
p.hash = h;
// 返回交换数据
return v;
}

你很迷糊,当然了,我看了那么久还是很迷糊

4. arenaExchange

多槽位的代码 我们也简单看一下

private final Object arenaExchange(Object item, boolean timed, long ns) {
// 获取多槽的槽
Node[] a = arena;
// 同理获取Node
Node p = participant.get();
// 因为这里是多个槽 所以加了个循环
//
for (int i = p.index;;) { // access slot at i
int b, m, c; long j;
// 这个j是原生的偏移量 也就是内存偏移量 不是简单的java下标
Node q = (Node)U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE);
// 如果槽位有数据 进行与单槽位相同的操作
if (q != null && U.compareAndSwapObject(a, j, q, null)) {
Object v = q.item; // release
q.match = item;
Thread w = q.parked;
if (w != null)
U.unpark(w);
return v;
}
// m是最大有效下标 m = bound是最大有效位置 & mask
// 并且 q == null 也就是对应槽位没有货物
else if (i <= (m = (b = bound) & MMASK) && q == null) {
// 交换物放到保险箱 与单槽操作一样
p.item = item;
// 与单槽的设置solt操纵一样 这里是放到多槽的某个槽里边
// 如果cas设置成功了 就等着检测match属性
if (U.compareAndSwapObject(a, j, null, p)) {
// 超时时间点
long end = (timed && m == 0) ? System.nanoTime() + ns : 0L;
// 当前线程
Thread t = Thread.currentThread();
//=================================================
// 这一块的操作 与单槽操作 如出一辙
// 1. 循环几次看检测match
// 2. 循环完了判断线程是否中单 是否超时 (没有是否多槽了 因为本身就是多槽 哈哈)
// 3. 循环完了还是没有 那就park 设置一下属性数据 等着被unpark
//
for (int h = p.hash, spins = SPINS;;) {
Object v = p.match;
// 自旋的时候 保险箱有货物了 也就是有人跟我交换了
if (v != null) {
// 获取到物品 清空一些基础数据
U.putOrderedObject(p, MATCH, null);
p.item = null; // clear for next use
p.hash = h;
return v;
}
else if (spins > 0) {
h ^= h << 1; h ^= h >>> 3; h ^= h << 10; // xorshift
if (h == 0) // initialize hash
h = SPINS | (int)t.getId();
else if (h < 0 && // approx 50% true
(--spins & ((SPINS >>> 1) - 1)) == 0)
Thread.yield(); // two yields per wait
}
else if (U.getObjectVolatile(a, j) != p)
spins = SPINS; // releaser hasn't set match yet
else if (!t.isInterrupted() && m == 0 &&
(!timed ||
(ns = end - System.nanoTime()) > 0L)) {
U.putObject(t, BLOCKER, this); // emulate LockSupport
p.parked = t; // minimize window
if (U.getObjectVolatile(a, j) == p)
U.park(false, ns);
p.parked = null;
U.putObject(t, BLOCKER, null);
}
else if (U.getObjectVolatile(a, j) == p &&
U.compareAndSwapObject(a, j, p, null)) {
if (m != 0) // try to shrink
U.compareAndSwapInt(this, BOUND, b, b + SEQ - 1);
p.item = null;
p.hash = h;
i = p.index >>>= 1; // descend
if (Thread.interrupted())
return null;
if (timed && m == 0 && ns <= 0L)
return TIMED_OUT;
break; // expired; restart
}
}
//=================================================
}
else
//占据槽位失败,先清空item,防止成功交换数据后,p.item还引用着item
p.item = null;
}
else {
// cas从箱子里拿货物被别人抢到了
// 其实这里很迷 这里他不是用的简单的循环i 而是用了bound的一个操作
// m是个多槽位的最大下标值
if (p.bound != b) { // stale; reset
p.bound = b;// 重置bound
p.collides = 0;// 这里是某个bound的失败次数
i = (i != m || m == 0) ? m : m - 1;//i到了最大值就递减
}
else if ((c = p.collides) < m || m == FULL ||
!U.compareAndSwapInt(this, BOUND, b, b + SEQ + 1)) {
p.collides = c + 1;
i = (i == 0) ? m : i - 1; // cyclically traverse
}
else
i = m + 1;//i 递增
p.index = i;// 更新p的index为i
}
}
}

看了多槽更迷糊了,我提议,看一下单槽先了解单槽

图片链接:

​https://www.processon.com/view/link/614f05e55653bb0d3eef5915​

获取密码关注公众号:木子的昼夜编程 发送:exchange流程图

无间道——程序员版_java_06

八、唠唠

这个很难的啊 看了很旧 不能说是深入了解吧,但是肯定对大脑内的思想有一定的提升。