Exchanger源码分析

基于jdk1.8源码进行分析的。

Exchanger用于两个线程之间交换数据。

继承结构

public class Exchanger<V>

可以简单没有继承什么类,也没实现什么接口。

构造方法

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

构造方法实例化Participant类。

成员方法

exchange(V x)

等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。

    @SuppressWarnings("unchecked")
    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;
    }

如果另一个线程已经在交换点等待,则出于线程调度目的,继续执行此线程,并接收当前线程传入的对象。当前线程立即返回,接收其他线程传递的交换对象。

如果还没有其他线程在交换点等待,则出于调度目的,禁用当前线程,且在发生以下两种情况之一前,该线程将一直处于休眠状态:

  1. 其他某个线程进入交换点
  2. 其他某个线程中断当前线程

如果当前线程在进入此方法时已经设置了该线程的中断状态或者 在等待交换时被中断,则抛出 InterruptedException,并且清除当前线程的已中断状态。

 exchange(V x, long timeout, TimeUnit unit)

等待另一个线程到达此交换点(除非当前线程被中断,或者超出了指定的等待时间),然后将给定的对象传送给该线程,同时接收该线程的对象。

    @SuppressWarnings("unchecked")
    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;
    }

上面这么说可能还是太抽象了,下面我们写一个DEMO演示,我们继续往下看。

package demo;

import java.util.concurrent.Exchanger;

/**
 * Exchanger测试例程
 * @ClassName:   ExchangerDemo  
 * @Description: TODO
 * @author       BurgessLee
 * @date         2019年4月28日  
 *
 */
public class ExchangerDemo {
	
	public static void main(String[] args) {
		Exchanger<String> exchanger = new Exchanger<String>();
		//当前一个线程执行的时候中断该线程,那么此时挂起的线程直接抛出异常结束
		Thread t0 = new Thread(new Runnable() {
			
			@Override
			public void run() {
				System.out.println(Thread.currentThread().getName() + "线程执行,交换前数据为1");
				try {
					String exchangeRes = exchanger.exchange("1");
					System.out.println(Thread.currentThread().getName() +"-res=" + exchangeRes);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		},"Thread0");
		t0.start();
		t0.interrupt();
		
		//单个线程执行的时候,直接挂起
//		Thread t0 = new Thread(new Runnable() {
//			
//			@Override
//			public void run() {
//				System.out.println(Thread.currentThread().getName() + "线程执行,交换前数据为1");
//				try {
//					String exchangeRes = exchanger.exchange("1");
//					System.out.println(Thread.currentThread().getName() +"-res=" + exchangeRes);
//				} catch (InterruptedException e) {
//					e.printStackTrace();
//				}
//			}
//		},"Thread0");
//		t0.start();
		
		//两个线程完成数据交换示例DEMO
//		Thread t0 = new Thread(new Runnable() {
//			
//			@Override
//			public void run() {
//				System.out.println(Thread.currentThread().getName() + "线程执行,交换前数据为1");
//				try {
//					String exchangeRes = exchanger.exchange("1");
//					System.out.println(Thread.currentThread().getName() +"-res=" + exchangeRes);
//				} catch (InterruptedException e) {
//					e.printStackTrace();
//				}
//			}
//		},"Thread0").start();
//		new Thread(new Runnable() {
//			
//			@Override
//			public void run() {
//				System.out.println(Thread.currentThread().getName() + "线程执行,交换前数据为2");
//				try {
//					String exchangeRes = exchanger.exchange("2");
//					System.out.println(Thread.currentThread().getName() +"-res=" + exchangeRes);
//				} catch (InterruptedException e) {
//					e.printStackTrace();
//				}
//			}
//		},"Thread1").start();
	}
	
}

DEMO中演示了几种情况,分别是

  1. 两个线程执行的时候,完成两个线程之间的数据交换,直接结束
  2. 单个线程执行,完事被挂起
  3. 单个线程执行,完事中断该线程,直接抛出异常结束

Exchanger 有单槽位和多槽位之分,单个槽位在同一时刻只能用于两个线程交换数据,这样在竞争比较激烈的时候,会影响到性能,多个槽位就是多个线程可以同时进行两个的数据交换,彼此之间不受影响,这样可以很好的提高吞吐量。 单槽 Exchanger相对要简单许多,我们通过源码分析来看,一共该类有两个内部类,下面我们看一下具体的源码。

Node内部类:

    @sun.misc.Contended static final class Node {
        //arena的下标,多个槽位的时候利用
        int index;              // Arena index
        //上一次记录的Exchanger.bound;
        int bound;              // Last recorded value of Exchanger.bound
        //在当前bound下CAS失败的次数;
        int collides;           // Number of CAS failures at current bound
        //用于自旋;
        int hash;               // Pseudo-random for spins
        //这个线程的当前项,也就是需要交换的数据;
        Object item;            // This thread's current item
        //交换的数据
        volatile Object match;  // Item provided by releasing thread
        //线程
        volatile Thread parked; // Set to this thread when parked, else null
    }

Participant: 

    /** The corresponding thread local class */
    static final class Participant extends ThreadLocal<Node> {
        public Node initialValue() { return new Node(); }
    }

这个类也就是我们通过构造方法创建Exchanger实例化出来的类,可以见到,该类继承自ThreadLocal类,且只有一个方法用来返回一个Node实例,所以可以想到此时的Node就代表一个线程并且是线程安全的。

里面我们还看到两个成员属性定义:

//如果交换的数据为 null,则用NULL_ITEM  代替
private static final Object NULL_ITEM = new Object();
//如果超时,用timed_out替代
private static final Object TIMED_OUT = new Object();

其他成员属性

//当前CPU可以用的进程数目
private static final int NCPU = Runtime.getRuntime().availableProcessors();
//自旋次数
private static final int SPINS = 1 << 10;
//用于交换数据的槽位
private volatile Node slot; 

下面我们再来重新看一下exchange方法。

exchange(V x)

不带时间参数的的成员方法,当然还有一个带有时间参数的exchange方法,此处就不在介绍,以为相比之下,就在月是否有时间限制,即超时判断。

    @SuppressWarnings("unchecked")
    public V exchange(V x) throws InterruptedException {
        //定义变量
        Object v;
        //如果x为空那么直接用NULL_ITEM替代,否则item=x
        //进行数据校验控制
        Object item = (x == null) ? NULL_ITEM : x;
        //private volatile Node[] arena;
        //arena是一个node数组,而且是标记有volatile关键字
        //单槽操作通过slotExchange实现的
        //arena为空,说明是单槽操作,如果不为空,那么是多槽操作,单槽操纵失败
        //多槽操作是通过arenaExchange实现的
        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方法的实现,这个方法代码稍微有点长。

    private final Object slotExchange(Object item, boolean timed, long ns) {
        //从ThreadLocal获取Node节点
        Node p = participant.get();
        //记录当前线程
        Thread t = Thread.currentThread();
        //如果当前线程中断,返回NULL
        if (t.isInterrupted())
            return null;
    
        //死循环
        for (Node q;;) {
            //private volatile Node slot;
            //槽位solt不为null,则说明已经有线程在这里等待交换数据了
            //那么此时进行交换数据,如果交换成功,直接返回。
            if ((q = slot) != null) {
                //重置槽位
                if (U.compareAndSwapObject(this, SLOT, q, null)) {
                    //获取交换的数据
                    Object v = q.item;
                    //等待线程需要的数据
                    q.match = item;
                    //等待线程
                    Thread w = q.parked;
                    //唤醒等待的线程
                    if (w != null)
                        U.unpark(w);
                    //返回的是v,也就是需要交换的数据
                    return v;
                }
                //存在竞争,其它线程抢先了一步该线程,因此需要采用多槽位模式,这个后面再分析
                //实例化arena,然后继续执行循环,知道slot为空或者上面不为空情况,交换成功
                if (NCPU > 1 && bound == 0 &&
                    U.compareAndSwapInt(this, BOUND, 0, SEQ))
                    arena = new Node[(FULL + 2) << ASHIFT];//可能继续循环,或者往下执行
            }
            //多槽位不为空,需要执行多槽位交换
            else if (arena != null)
                return null;//直接返回了,说明单槽交换失败了
            //到这里slot为空,arena为空
            //只有当前线程,所以自己占槽位,并维护对应的状态值
            else {//还没有其他线程来占据槽位
                p.item = item;
                //设置槽位为p(也就是槽位被当前线程占据)
                //槽位占成功,那么退出循环
                if (U.compareAndSwapObject(this, SLOT, null, p))
                    break;//退出无限循环  --->往西继续执行
                //如果设置槽位失败,则有可能其他线程抢先了,重置item,重新循环
                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)) == 0)
                    // 主动让出cpu,这样可以提供cpu利用率
                    //(反正当前线程也自旋等待,还不如让其它任务占用cpu)
                    Thread.yield();
            }
            //其它线程来交换数据了,修改了solt,但是还没有设置match,再稍等一会
            else if (slot != p)
                spins = SPINS;
            //需要阻塞等待其它线程来交换数据
            //没发生中断,并且是单槽交换,没有设置超时或者超时时间未到 则继续执行
            else if (!t.isInterrupted() && arena == null &&
                     (!timed || (ns = end - System.nanoTime()) > 0L)) {
                //cas设置BLOCKER,可以参考Thread 中的parkBlocker
                U.putObject(t, BLOCKER, this);
                //需要挂起当前线程
                p.parked = t;
                if (slot == p)
                    U.park(false, ns);//阻塞当前线程
                //被唤醒后    
                p.parked = null;
                //清空BLOCKER
                U.putObject(t, BLOCKER, null);
            }
            //不满足前面 else if 条件,交换失败,需要重置solt
            else if (U.compareAndSwapObject(this, SLOT, p, null)) {
                v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null;
                break;
            }
        }
        //清空match
        U.putOrderedObject(p, MATCH, null);
        p.item = null;
        p.hash = h;
        //返回交换得到的数据(失败则为null)
        return v;
    }

1.当一个线程来交换数据时,如果发现槽位(solt)有数据时,说明其它线程已经占据了槽位,等待交换数据,那么当前线程就和该槽位进行数据交换,设置相应字段。

2.如果交换失败,则说明其它线程抢先了该线程一步和槽位交换了数据,那么这个时候就存在竞争了,这个时候就会生成多槽位(area),后面就会进行多槽位交换了。 
3.如果来交换的线程发现槽位没有被占据,啊哈,这个时候自己就把槽位占据了,如果占据失败,则有可能其他线程抢先了占据了槽位,重头开始循环。 
4.当来交换的线程占据了槽位后,就需要等待其它线程来进行交换数据了,首先自己需要进行一定时间的自旋,因为自旋期间有可能其它线程就来了,那么这个时候就可以进行数据交换工作,而不用阻塞等待了;如果不幸,进行了一定自旋后,没有其他线程到来,那么还是避免不了需要阻塞(如果设置了超时等待,发生了超时或中断异常,则退出,不阻塞等待)。当准备阻塞线程的时候,发现槽位值变了,那么说明其它线程来交换数据了,但是还没有完全准备好数据,这个时候就不阻塞了,再稍微等那么一会;如果始终没有等到其它线程来交换,那么就挂起当前线程。 
5.当其它线程到来并成功交换数据后,会唤醒被阻塞的线程,阻塞的线程被唤醒后,拿到数据(如果是超时,或中断,则数据为null)返回,结束。

以上就是单槽过程的分析了。下面我们看多槽位情况分析。

多槽位呢,实际就是一个Node 数组,代表了很多的槽位。同样内部类Node就不在粘贴代码了。

//用来避免伪共享的情况  关于伪共享,不理解的可以自行搜索一下
//1 << ASHIFT 可以避免两个Node在同一个共享区
private static final int ASHIFT = 7;

初始化arena 时会设置bound为SEQ(SEQ=MMASK + 1)。MMASK 值为255,二进制:8个1 
arena的大小为(FULL + 2) << ASHIFT,因为1 << ASHIFT 是用于避免伪共享的,因此实际有效的Node 只有FULL + 2 个,这个我们从后面的代码也可以得出。

                // 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];

下面我们看一下多曹情况具体方法的实现。

arenaExchange

private final Object arenaExchange(Object item, boolean timed, long ns) {
    // 槽位数组
    Node[] a = arena;
    //代表当前线程的Node
    Node p = participant.get(); // p.index 初始值为 0
    for (int i = p.index;;) {                      // access slot at i
        int b, m, c; long j;                       // j is raw array offset
        //在槽位数组中根据"索引" i 取出数据 j相当于是 "第一个"槽位
        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;
        }
        // bound 是最大的有效的 位置,和MMASK相与,得到真正的存储数据的索引最大值
        else if (i <= (m = (b = bound) & MMASK) && q == null) {
            // i 在这个范围内,该槽位也为空

            //将需要交换的数据 设置给p
            p.item = item;                         // offer
            //设置该槽位数据(在该槽位等待其它线程来交换数据)
            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) {
                        //交换数据后,清空部分设置,返回交换得到的数据,over
                        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
                    }
                    // 交换数据的线程到来,但是还没有设置好match,再稍等一会
                    else if (U.getObjectVolatile(a, j) != p)
                        spins = SPINS; 
                    //符合条件,特别注意m==0 这个说明已经到达area 中最小的存储数据槽位了
                    //没有其他线程在槽位等待了,所有当前线程需要阻塞在这里     
                    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
                        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
                        // 发送中断,返回null
                        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;                     // clear offer
        }
        else { // i 不在有效范围,或者被其它线程抢先了
            //更新p.bound
            if (p.bound != b) {                    // stale; reset
                p.bound = b;
                //新bound ,重置collides
                p.collides = 0;
                //i如果达到了最大,那么就递减
                i = (i != m || m == 0) ? m : m - 1;
            }
            else if ((c = p.collides) < m || m == FULL ||
                     !U.compareAndSwapInt(this, BOUND, b, b + SEQ + 1)) {
                p.collides = c + 1; // 更新冲突
                // i=0 那么就从m开始,否则递减i
                i = (i == 0) ? m : i - 1;          // cyclically traverse
            }
            else
                //递增,往后挪动
                i = m + 1;                         // grow
            // 更新index
            p.index = i;
        }
    }
}

整体执行逻辑较为复杂,此处贴上给出的思路。

1. 从场地中选出偏移地址为(i << ASHIFT) + ABASE的内存值,也即第i个真正可用的Node,判断其槽位是否为空,为空,进入【步骤2】;不为空,说明有线程在此等待,尝试抢占该槽位,抢占成功,交换数据,并唤醒等待线程,返回,结束;没有抢占成功,进入【步骤9】

2. 检查索引(i vs m)是否越界,越界,进入【步骤9】;没有越界,进入下一步。

3. 尝试占有该槽位,抢占失败,进入【步骤1】;抢占成功,进入下一步。

4. 检查match,是否有线程来交换数据,如果有,交换数据,结束;如果没有,进入下一步。

5. 检查spin是否大于0,如果不大于0,进入下一步;如果大于0,检查hash是否小于0,并且spin减半或为0,如果不是,进入【步骤4】;如果是,让出CPU时间,过一会儿,进入【步骤4】

6. 检查是否中断,m达到最小值,是否超时,如果没有中断,没有超时,并且m达到最小值,阻塞,过一会儿进入【步骤4】;否则,下一步。

7. 没有线程来交换数据,尝试丢弃原有的槽位重新开始,丢弃失败,进入【步骤4】;否则,下一步。

8. bound减1(m>0),索引减半;检查是否中断或超时,如果没有,进入【步骤1】;否则,返回,结束。

9. 检查bound是否发生变化,如果变化了,重置collides,索引重置为m或左移,转向【步骤1】;否则,进入下一步。

10. 检查collides是否达到最大值,如果没有,进入【步骤13】,否则下一步。

11. m是否达到FULL,是,进入【步骤13】;否则,下一步。

12. CAS bound加1是否成功,如果成功,i置为m+1,槽位增长,进入【步骤1】;否则,下一步。

13. collides加1,索引左移,进入【步骤1】

来自http://www.cnblogs.com/aniao/p/aniao_exchanger.html

以上就是此次针对Exchanger的源码分析过程了。如果有不对的地方还请指正,谢谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值