JUC 之 队列 Exchanger 详解

Exchanger是一个Java并发工具类,用于在两个线程间同步交换数据。它维护了一个同步点,在这个点上线程可以配对并交换它们的数据。文章详细介绍了Exchanger的核心成员如arena数组、slot和Node结构,以及exchange和slotExchange方法的工作原理,特别强调了在多线程竞争下的性能优化,如避免缓存行冲突和自旋等待策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JUC 之 Exchanger 详解

在一个同步点上,线程可以配对和交换成对中的元素。每个线程在进入exchange方法时携带自身数据,与另一线程进行匹配,配对成功时两个线程相互交换数据并返回对方的数据。交换器可以看作是双向形式的同步队列。

核心成员介绍

public Exchanger() {
    participant = new Participant();
}
// 用于计算 arena 数组中两个可用数组项之间的间隔字节数,大小至少是一个缓存行的大小
private static final int ASHIFT = 7;
// arena 支持的最大索引个数 0xff = 1111 1111 = 255
private static final int MMASK = 0xff;
// bound 发生变更时递增 MMASK + 1 = 1 0000 0000 = 256,每次递增都是同时增加高 8 位和 低 8 位,即同时对 版本号 加 1,数组有效索引个数 加 1
private static final int SEQ = MMASK + 1;
private static final int NCPU = Runtime.getRuntime().availableProcessors();
// arena 所能容纳的最大有效索引个数
static final int FULL = (NCPU >= (MMASK << 1)) ? MMASK : NCPU >>> 1;
// 自旋次数
private static final int SPINS = 1 << 10;
// 保存数据的节点
private volatile Node[] arena;
// 当没有发生线程竞争时用于数据交换
private volatile Node slot;
// arena 数组最大有效位索引,每次更新 arena 时进行更新,初始为 0 ,保证 arena 数组只被初始化一次
private volatile int bound;

@sun.misc.Contended static final class Node {
    int index;              // node 所处 arena 数组的下标
    int bound;              // 记录 Exchanger 的 bound
    int collides;           // 记录 CAS 失败的次数
    int hash;               
    Object item;            // 保存带交换的数据
    volatile Object match;  // 保存其它线程交换成功的数据
    volatile Thread parked; // 保存当前线程信息,用于交换线程唤醒使用
}

核心方法介绍

exchange 方法
public V exchange(V x) throws InterruptedException {
    Object v;
    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 方法

现成竞争不激烈的情况,通过一个 slot 操作就能完成匹配交换数据行为。当发生竞争激烈的时候,线程将会转移到 arena 数组的操作上。

所以在此方法中如果发现线程竞争激烈将要负责创建 arena 数组,数组创建完成后,需要唤醒那个正在等待 slot 交换的线程,让其也加入到对 arena 数组的操作上(arenaExchange 方法)。

private final Object slotExchange(Object item, boolean timed, long ns) {
    Node p = participant.get();// 初始化一个 node 节点
    Thread t = Thread.currentThread();
    if (t.isInterrupted()) // 该方法检测线程是否被中断,不会清除中断标志位,当被中断时,返回 null,在 exchange 方法中将被检测到然后抛出 InterruptedException 异常
        return null;
	// 该 for(;;) 循环中存在一下几种情况
    // 1、slot == null,代表你是第一个进入的线程,直接将携带的数据 保存到上面初始化的 p 中,然后将 slot 设置为 p,退出该循环等待其它线程过来交换数据
    // 2、slot 以初始化,CAS 尝试进行数据交换,CAS 失败表明已被其它线程交换走了,也即线程竞争激烈,那么该线程将负责 初始化 arena 数组,然后该线程会进入 arenaExchange 方法中进行数据交换
    for (Node q;;) {
        if ((q = slot) != null) {// slot 已初始化,并将 slot 赋值给到 q,保存在栈上
            if (U.compareAndSwapObject(this, SLOT, q, null)) {
                // CAS 尝试将 slot 置空,成功表明该线程完成了匹配,下面进行数据交换操作
                Object v = q.item;
                q.match = item;// 将交换数据赋值给 match (待匹配线程)
                Thread w = q.parked; // 判断 待匹配线程是否已经调用了 park 方法进入 OS 阻塞,是 唤醒之
                if (w != null) U.unpark(w);
                return v;// 匹配成功返回
            }
            // CPU 核心数大于 1,bound 变量用于控制 arena 数组只会被创建一次,
            if (NCPU > 1 && bound == 0 &&
                U.compareAndSwapInt(this, BOUND, 0, SEQ))// SEQ = 0x100,
                arena = new Node[(FULL + 2) << ASHIFT];// 创建 arena 数组
        }
        else if (arena != null) return null; // arena != null 代表线程竞争激烈,应该进入 arenaExchange 方法进行数据匹配交换
        else {// arena == null 尝试将 数据 放入 slot,CAS 失败说明有线程竞争,则重复for(;;)循环 
            p.item = item;
            if (U.compareAndSwapObject(this, SLOT, null, p)) break;
            p.item = null;
        }
    }

    int h = p.hash;
    long end = timed ? System.nanoTime() + ns : 0L;// 如果是超时等待,则计算超时等待时间
    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)/*0x 1ff */) == 0)// spins 低 9 位等于 0 时让出 CPU 执行权
                Thread.yield();
        }
        else if (slot != p) spins = SPINS;// 说明 slot 已经被匹配
        else if (!t.isInterrupted() && arena == null && // 线程未被中断,且 arena 为 null(arena 不为 null时表示其它线程都去 arenaExchange 方法进行数据交换操作了,不会再有线程去操作 slot)
                 (!timed || (ns = end - System.nanoTime()) > 0L)) {
            // 暂未有线程进来与之匹配交换数据,将进入 OS 阻塞
            U.putObject(t, BLOCKER, this);// 通过 jstack 命令可看到 当前线程阻塞在哪个对象上
            p.parked = t;// 保存线程信息,用于交换线程唤醒使用
            if (slot == p)
                U.park(false, ns);
            p.parked = null;
            U.putObject(t, BLOCKER, null);// 唤醒后清空 BLOCKER 对象
        }
        else if (U.compareAndSwapObject(this, SLOT, p, null)) {// 走到这里说明 arena 已经被创建,那么清空 slot ,让其进入 arenaExchange 方法完成数据交换操作
            // 如果是因为超时导致的返回,设置返回值 v 为 TIMED_OUT = new Object(),否则设置为 null 并结束循环
            v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null;
            break;
        }
    }
    // 交换成功,使用 putOrderedObject 优化 volatile 变量,只需保证 后面的写操作不会重排序即可,不需要保证 match 的可见性
    U.putOrderedObject(p, MATCH, null);// 清空 match 的值
    p.item = null;
    p.hash = h;
    return v;
}
arenaExchange 方法

当在多线程高并发的场景下,需要交换的数据过多,单个 slot 无法满足时将会切换到 arena 数组中进行数据交换,此时 slot 槽就没有存在的意义了,我们使用 arena 数组来承载更多的线程在无法交换数据时的阻塞等待操作。每个线程首次进入该方法时都会从各自的 ThreadLocal(participant) 中获取一个 node 节点信息,此时初始 index == 0,所以每个首次进来的线程都将去竞争 arena 数组中同一个索引槽的 node 信息,自然就会有线程竞争失败而导致重试,为了提升性能,每次重试并不是按照数组下标递增的方式寻找下一个可用数组项,而是向前跳过一定的数组项找到一个可用的项,也即打散每个线程所占用的数组项(因为数组是地址连续的,如果两个线程使用的数组项是挨在一起的,那么就有可能同时被加载到 CPU 的同一个缓存行中,这样就会导致两个线程并发操作时要对缓存行上锁从而降低了性能)

private final Object arenaExchange(Object item, boolean timed, long ns) {
    Node[] a = arena;
    Node p = participant.get();// 每个线程首次进入该方法获取到的 node 节点 index 默认都是从 0 开始
    for (int i = p.index;;) {                      // access slot at i
        int b, m, c; long j;                       // j is raw array offset
        // 获取 arena 数组 i 处的数组项的 node 信息,此处使用的是 Unsafe 类的 getObjectVolatile 方法,其它线程修改了当前线程可以立即看到修改值。(通过 Unsafe 的 xxxVolatile 方法可以让一个普通变量拥有 volatile 语义)
        Node q = (Node)U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE);
        // q != null 说明有其它线程放入了数据,那么 CAS 尝试与之进行匹配交换,CAS 成功则 匹配成功
        if (q != null && U.compareAndSwapObject(a, j, q, null)) {
            Object v = q.item;    // 获取 q 的 item 值                 
            q.match = item;  // 将自己的数据赋值给 q 的 match 属性
            Thread w = q.parked;
            if (w != null) U.unpark(w); // 如果待匹配线程以调用 park 方法去阻塞了,唤醒之
            return v; // 当前线程返回
        }
        // 首次进入 for 循环 第一个判断恒成立,q == null 代表该位置还未被使用,那么尝试将自己的数据 放入其中,等待其它线程与之匹配
        else if (i <= (m = (b = bound) & MMASK) && q == null) {
            p.item = item; 
            // CAS 成功,位置抢占成功,下面代码处理逻辑同 slotExchange 方法,
            if (U.compareAndSwapObject(a, j, null, 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;
                    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; 
                        if (h == 0)    h = SPINS | (int)t.getId();
                        else if (h < 0 &&   (--spins & ((SPINS >>> 1) - 1)) == 0)
                            // 达到某个特定值时 让出 CPU 执行权,
                            Thread.yield();        
                    }
                    else if (U.getObjectVolatile(a, j) != p)// 说明有线程正在与之匹配,
                        spins = SPINS;       
                    else if (!t.isInterrupted() && m == 0 &&
                             (!timed ||
                              (ns = end - System.nanoTime()) > 0L)) {
                        // 进行阻塞等待匹配线程与之匹配
                        U.putObject(t, BLOCKER, this); 
                        p.parked = t;              
                        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)// m != 0 表示目前使用的数组项存在空余,尝试减少有效数组项的个数,               
                            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
                p.item = null;    // CAS 失败 清空 数据
        }
        else {
            // 此 else 分支主要目的在于打散 arena 数组中的有效 slot 槽,
            // 此时 p 的 bound 的值和当前 bound 值不一致,表明当前线程node 节点所在的 arena 的数组有效 slot 已经被更新,重新记录 bound 和 collides ,然后更新 i 值重试
            if (p.bound != b) {   
                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
                i = m + 1;                         // grow
            p.index = i;
        }
    }
}

总结:通过源码我们看到在没有现成竞争的情况下通过一个 slot 槽就能完成数据交换操作,次过程在 slotExchange 方法中完成,当发生线程竞争时,在该方法中将会创建 arena 数组(后续数组中的每个数组项简称一个 slot 槽),通过 Exchanger 的 bound 变量控制数组只会被初始化一次。arena 数组初始化时候线程将从 该方法退出进而进入 arenaExchange 方法中去完成数据交换操作。arena 数组中的数组项并没有被全部用来做数据交换使用,而是使用 FULL 对其进行了限制,只能使用 FULL 个数组项(FULL = (NCPU >= (MMASK << 1)) ? MMASK : NCPU >>> 1,一般情况 FULL 等于 NCPU 个数 / 2 )。线程在使用 arena 数组中的 slot 槽时,每个线程所使用的 slot 是间隔开的,每个相邻的 slot 之间通过字节数填充,使其每个所占用的字节大小等于 CPU 缓存行的大小,避免多个 slot 被加载到同一个缓存行,进而导致性能下降。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值