【JUC系列】同步工具类之Exchanger

Exchanger


在一个同步点,两个线程可以在该点配对和交换对中的数据。这两个线程通过exchange()方法交换数据,当一个线程先执行exchange()方法后,它会一直等待第二个线程也执行exchange()方法,当这两个线程到达同步点时,这两个线程就可以交换数据了。

Exchanger 可以看作是 SynchronousQueue 的双向形式。

示例用法:以下是一个类的亮点,它使用 Exchanger 在线程之间交换缓冲区,以便填充缓冲区的线程在需要时获得一个新清空的缓冲区,并将填充的缓冲区交给清空缓冲区的线程。

示例

import java.text.SimpleDateFormat;
import java.util.Date;
import ja va.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;

public class ExchangerDemo {

    static class ThreadA extends Thread {
        private final Exchanger<String> exchanger;
        private String data = "A";

        ThreadA(Exchanger<String> exchanger) {
            this.exchanger = exchanger;
        }

        ThreadA(String data, Exchanger<String> exchanger) {
            this.data = data;
            this.exchanger = exchanger;
        }

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "] " + "Before exchange ThreadA's data is " + data);
                data = exchanger.exchange(data);
                System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "] " + "After exchange ThreadA's data is " + data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class ThreadB extends Thread {
        private final Exchanger<String> exchanger;
        private String data = "B";

        ThreadB(Exchanger<String> exchanger) {
            this.exchanger = exchanger;
        }

        ThreadB(String data, Exchanger<String> exchanger) {
            this.data = data;
            this.exchanger = exchanger;
        }

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "] " + "Before exchange ThreadB's data is " + data);
                data = exchanger.exchange(data);
                System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "] " + "After exchange ThreadB's data is " + data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Exchanger<String> exchanger = new Exchanger<>();
        new ThreadA("A1", exchanger).start();
        new ThreadB("B1", exchanger).start();
    }
}

执行结果

[15:56:26] Before exchange ThreadB's data is B1
[15:56:26] Before exchange ThreadA's data is A1
[15:56:26] After exchange ThreadB's data is A1
[15:56:26] After exchange ThreadA's data is B1

核心算法

Created with Raphaël 2.3.0 Start 数据槽slot是否有值? 将item 设置到Node 中 CAS操作(将node更新item)是否成功 阻塞,等待被唤醒 被唤醒后 返回node中匹配到的item End CAS操作(将slot中node重置为null)是否成功 获取node中的数据 将自己的值设置到node的match 唤醒等待的线程 yes no yes no yes no

伪代码

for (;;) {
    if (slot is empty) { // offer
        // slot为空时,将item 设置到Node 中        
        place item in a Node;
        if (can CAS slot from empty to node) {
            // 当将node通过CAS交换到slot中时,挂起线程等待被唤醒
            wait for release;
            // 被唤醒后返回node中匹配到的item
            return matching item in node;
        }
    } else if (can CAS slot from node to empty) { // release
         // 将slot设置为空
        // 获取node中的item,将需要交换的数据设置到匹配的item
        get the item in node;
        set matching item in node;
        // 唤醒等待的线程
        release waiting thread;
    }
    // else retry on CAS failure
}

组成

内部类Participant

继承了ThreadLocal

    // 对应的线程本地类--初始化中构造了Node
    static final class Participant extends ThreadLocal<Node> {
        public Node initialValue() { return new Node(); }
    }

内部类Node

    // 节点保存部分交换的数据,以及其他每个线程的标记。 通过@sun.misc.Contended 填充以减少内存争用。
	@sun.misc.Contended static final class Node {
        int index;              // Arena index -- Arena 下标 slot插槽用
        int bound;              // Last recorded value of Exchanger.bound 上一次记录的Exchanger.bound
        int collides;           // Number of CAS failures at current bound 在当前bound下CAS失败的次数
        int hash;               // Pseudo-random for spins 自旋的伪随机数
        Object item;            // This thread's current item 线程的当前项,需要交换的数据
        volatile Object match;  // Item provided by releasing thread 做releasing操作的线程传递的项 释放线程提供的项
        volatile Thread parked; // Set to this thread when parked, else null 阻塞时设置为该线程,否则为空
    }

@sun.misc.Contended 伪共享解决方案

需要注意的是在启动jvm的时候要加入-XX:-RestrictContended

成员变量

// 每线程状态
private final Participant participant;
// 默认值为null 直到出现多个参与者使用同一个交换场所时,会存在严重伸缩性问题。既然单个交换场所存在问题,那么我们就安排多个,也就是数组arena。通过数组arena来安排不同的线程使用不同的slot来降低竞争问题,并且可以保证最终一定会成对交换数据。但是Exchanger不是一来就会生成arena数组来降低竞争,只有当产生竞争是才会生成arena数组。
private volatile Node[] arena;
// 在检测到争用之前使用的插槽。
private volatile Node slot;
// 最大有效竞技场位置的索引,与高位的 SEQ 编号进行或运算,每次更新时递增。 从 0 到 SEQ 的初始更新用于确保 arena 数组只构造一次。
private volatile int bound;

// 任意两个已使用插槽之间的字节距离(作为移位值)。 1 << ASHIFT 至少应为高速缓存行大小。
private static final int ASHIFT = 7;
// 支持的最大arena index。 最大可分配的arena大小为 MMASK+1。必须是2减1的幂,小于 (1<<(31-ASHIFT))。 255的上限足以满足主要算法的预期扩展限制。
private static final int MMASK = 0xff;
// 绑定字段的序列/版本位的单位。 对边界的每次成功更改也会添加 SEQ。
private static final int SEQ = MMASK + 1;
// CPU的可用数
private static final int NCPU = Runtime.getRuntime().availableProcessors();
// arena 的最大槽索引:原则上可以容纳所有线程而不会发生争用的槽数,或最多可索引的最大值。
static final int FULL = (NCPU >= (MMASK << 1)) ? MMASK : NCPU >>> 1;
// 当 NCPU==1 时禁用旋转。等待match的自旋次数,1024,实际平均自旋次数为2*1024。
private static final int SPINS = 1 << 10;
// 空对象
private static final Object NULL_ITEM = new Object();
// 内部交换方法在超时时返回的哨兵值,以避免需要这些方法的单独定时版本。
private static final Object TIMED_OUT = new Object();

构造函数

    public Exchanger() {
        participant = new Participant();
    }

核心方法

方法名描述
exchange(V x) throws InterruptedException等待另一个线程到达这个交换点(除非当前线程被中断),然后将给定的对象传递给它,并接收它的对象作为回报。
如果另一个线程已经在交换点等待,则为了线程调度目的而恢复它并接收当前线程传入的对象。 当前线程立即返回,接收其他线程传递给交换器的对象。
如果没有其他线程已经在交换中等待,则当前线程被禁用以进行线程调度并处于休眠状态,直到发生以下两种情况之一:
1.其他线程进入交换
2.其他线程中断当前线程。

它是响应中断的,如果当前线程:
1.在进入此方法时设置其中断状态
2.在等待交换时被中断
会抛出 InterruptedException 并清除当前线程的中断状态
exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException等待另一个线程到达这个交换点(除非当前线程被中断或者指定的等待时间已经过去),然后将给定的对象传送给它,接收它的对象作为回报
如果另一个线程已经在交换点等待,则为了线程调度目的而恢复它并接收当前线程传入的对象。当前线程立即返回,接收其他线程传递给交换器的对象。
如果没有其他线程已经在交换中等待,则当前线程被禁用以进行线程调度并处于休眠状态,直到发生以下三种情况之一:
1.其他线程进入交换
2.其他线程中断当前线程。
3.指定的等待时间已过。

它是响应中断的,如果当前线程:
1.在进入此方法时设置其中断状态
2.在等待交换时被中断
会抛出 InterruptedException 并清除当前线程的中断状态。
如果指定的等待时间过去,则抛出 TimeoutException。如果时间小于或等于零,则该方法根本不会等待。
Object arenaExchange(Object item, boolean timed, long ns)启用arenas时的交换功能。
Object slotExchange(Object item, boolean timed, long ns)在启用arenas之前使用的交换功能。
方法调用说明

exchange(V x)

    public V exchange(V x) throws InterruptedException {
        Object v;
        // 参数如果是null 需要将item置为空的对象 子方法中不传入null
        Object item = (x == null) ? NULL_ITEM : x; // translate null args
        // 响应中断
        if ((arena != null ||
             (v = slotExchange(item, false, 0L)) == null) &&
            ((Thread.interrupted() || // disambiguates null return
              (v = arenaExchange(item, false, 0L)) == null)))
            throw new InterruptedException();
        return (v == NULL_ITEM) ? null : (V)v;
    }

slotExchange(Object item, boolean timed, long ns)

    private final Object slotExchange(Object item, boolean timed, long ns) {
        // 获取当前线程的node对象
        Node p = participant.get();
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 若线程中断 则直接返回null
        if (t.isInterrupted()) // preserve interrupt status so caller can recheck
            return null;
        // 自旋
        for (Node q;;) {
            // 获取slot赋予q,若此时slot不为null,表示此时的SLOT中
            if ((q = slot) != null) {
                // 此刻将通过将CAS操作 先将SLOT置null,若成功在进行数据交换
                if (U.compareAndSwapObject(this, SLOT, q, null)) {
                    // 获取Node中的item唤醒线程,给出参数中的item作为match
                    Object v = q.item;
                    q.match = item;
                    Thread w = q.parked;
                    if (w != null)
                        U.unpark(w);
                    return v;
                }
                // create arena on contention, but continue until slot null
                // CAS操作失败,若CPU个数大于0且bound尚未初始化,尝试CAS将BOUND将其增加SEQ
                if (NCPU > 1 && bound == 0 &&
                    U.compareAndSwapInt(this, BOUND, 0, SEQ))
                    // 初始化arena数组,
                    arena = new Node[(FULL + 2) << ASHIFT];
            }
            // 代码在此,说明slot为null,若数据数组槽非null这此方法不再执行,需要通过arenaExchange执行
            else if (arena != null)
                return null; // caller must reroute to arenaExchange
            // 逻辑在此处,slot和arena都为null
            else {
                // 进入此处的线程是某批次第一个交换的数据,需要将交换数据存放到p
                p.item = item;
                // 通过CAS操作将需要交换数据的node(p)赋给SLOT,
                // 若成功跳出自旋,
                // 若失败,需要清空p的item值,代表SLOT在此时被其他线程存放了数据,需要重新自旋获取SLOT的数据,进行交换
                if (U.compareAndSwapObject(this, SLOT, null, p))
                    break;
                p.item = null;
            }
        }

        // await release
        // 此时当前线程已经将数据存放到SLOT了,等待其他线程交换数据然后唤醒当前线程
        int h = p.hash;
        long end = timed ? System.nanoTime() + ns : 0L;
        // 确定自旋次数
        int spins = (NCPU > 1) ? SPINS : 1;
        Object v;
        // 第二个自旋,直到p.match!=null
        while ((v = p.match) == null) {
            // 下面的逻辑主要是自旋等待,直到spins递减到0为止
            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();
            }
            // 此时当前线程的节点与slot不一致?重新配置自旋次数,继续自旋,次数是两倍?
            else if (slot != p)
                spins = SPINS;
            // 此处表示未设置超时或者时间未超时未发上中断且交换的节点不是arena数组 
            else if (!t.isInterrupted() && arena == null &&
                     (!timed || (ns = end - System.nanoTime()) > 0L)) {
                // 设置线程t被当前对象阻塞
                U.putObject(t, BLOCKER, this);
                // 给p挂机线程的值赋值
                p.parked = t;
                if (slot == p)
                    // 如果slot还没有被置为null,也就表示暂未有线程过来交换数据,需要将当前线程挂起
                    U.park(false, ns);
                // 线程被唤醒,将被挂起的线程设置为null
                p.parked = null;
                // 设置线程t未被任何对象阻塞
                U.putObject(t, BLOCKER, null);
            }
            else if (U.compareAndSwapObject(this, SLOT, p, null)) {
                // arena不为null则v为null,其它为超时则v为超市对象TIMED_OUT,并且跳出循环
                v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null;
                break;
            }
        }
        // 取走match值,并将p中的match置为null
        U.putOrderedObject(p, MATCH, null);
        // 设置item为null
        p.item = null;
        p.hash = h;
        // 返回交换值
        return v;
    }

如何计算数据槽数组大小

arena = new Node[(FULL + 2) << ASHIFT];

int ASHIFT = 7;
NCPU = Runtime.getRuntime().availableProcessors();
int MMASK = 0xff;
int FULL = (NCPU >= (MMASK << 1)) ? MMASK : NCPU >>> 1;

假设NCPU=4,NCPU >>> 1 = 2
MMASK的值为255
MMASK << 1的值为254,会有NCPU的值超过这个数?不带可能吧
那么此时FULL=2(FULL + 2) << ASHIFT 即为 4<<7=4*2^7=512
arena数组槽的最大值就是512

arenaExchange(Object item, boolean timed, long ns)

此方法被执行时表示多个线程进入交换区交换数据,arena数组已被初始化,此方法中的一些处理方式和slotExchange比较类似,它是通过遍历arena数组找到需要交换的数据。

	// timed 为true表示设置了超时时间,ns为>0的值,反之没有设置超时时间
	private final Object arenaExchange(Object item, boolean timed, long ns) {
        Node[] a = arena;
        // 获取当前线程中的存放的node
        Node p = participant.get();
        // index初始值0
        for (int i = p.index;;) {                      // 遍历slot access slot at i
            // 遍历,如果在数组中找到数据则直接交换并唤醒线程,如未找到则将需要交换给其它线程的数据放置于数组中
            int b, m, c; long j;                       // j is raw array offset
            // q实际为arena数组偏移(i + 1) *  128个地址位上的node
            Node q = (Node)U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE);
             // 如果q不为null,并且CAS操作将下标j的元素置为null,若成功,说明找到对应的节点,进行数据交换并唤醒线程
            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;
            }
            // q 为null 并且 i 未超过数组边界
            else if (i <= (m = (b = bound) & MMASK) && q == null) {
                // 将用来跟其它线程的进行交换item赋值给p中的item
                p.item = item;                         // offer
                if (U.compareAndSwapObject(a, j, null, p)) {
                    // 成功对应i的数据槽放入p
                    long end = (timed && m == 0) ? System.nanoTime() + ns : 0L;
                    Thread t = Thread.currentThread(); // wait
                    // 自旋直到有其它线程进入,遍历到该元素并与其交换,同时当前线程被唤醒
                    for (int h = p.hash, spins = SPINS;;) {
                        Object v = p.match;
                        // 说明 // 其它线程设置的需要交换的数据match不为null
                        // 将match设置null,item设置为null
                        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
                        }
                        // j下标的节点不与当前线程一致,重新自旋可能是releaser未设置match
                        else if (U.getObjectVolatile(a, j) != p)
                            //和slotExchange方法中的类似,arena数组中的数据已被CAS设置,match值还未设置,让其再自旋等待match被设置
                            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)) {
                            // 这里给bound增加加一个SEQ
                            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
                    // 交换失败,表示有其它线程更改了arena数组中下标i的元素
                    p.item = null;                     // clear offer
            }
            else {
                // 此时表示下标不在bound & MMASK或q不为null但CAS操作失败
                // 需要更新bound变化后的值
                if (p.bound != b) {                    // stale; reset
                    p.bound = b;
                    p.collides = 0;
                    i = (i != m || m == 0) ? m : m - 1;
                }
                else if ((c = p.collides) < m || m == FULL ||
                         !U.compareAndSwapInt(this, BOUND, b, b + SEQ + 1)) {
                    // 记录CAS失败的次数
                    p.collides = c + 1;
                    // 循环遍历
                    i = (i == 0) ? m : i - 1;          // cyclically traverse
                }
                else
                    // 此时表示bound值增加了SEQ+1
                    i = m + 1;                         // grow
                // 设置下标
                p.index = i;
            }
        }
    }

exchange(V x, long timeout, TimeUnit unit)

exchange(V x)的区别就是增加了,超时异常。

    public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
        Object v;
        Object item = (x == null) ? NULL_ITEM : x;
        long ns = unit.toNanos(timeout);
        if ((arena != null ||
             (v = slotExchange(item, true, ns)) == null) &&
            ((Thread.interrupted() ||
              (v = arenaExchange(item, true, ns)) == null)))
            throw new InterruptedException();
        // 超时异常
        if (v == TIMED_OUT)
            throw new TimeoutException();
        return (v == NULL_ITEM) ? null : (V)v;
    }

参考:https://www.pdai.tech/md/java/thread/java-thread-x-juc-tool-exchanger.html

  • Exchanger主要解决什么问题?
  • 对比SynchronousQueue,为什么说Exchanger可被视为 SynchronousQueue 的双向形式?
  • Exchanger在不同的JDK版本中实现有什么差别?
  • Exchanger实现机制?
  • Exchanger已经有了slot单节点,为什么会加入arena node数组? 什么时候会用到数组?
  • arena可以确保不同的slot在arena中是不会相冲突的,那么是怎么保证的呢?
  • 什么是伪共享,Exchanger中如何体现的?
  • Exchanger实现举例
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值