Java并发学习(十八)-并发工具Exchanger

断断续续看了一个多礼拜,Exchanger总算是看明白了,思想不难,但是不理解思想去看代码就比较难了。
下面慢慢学习。

What is Exchanger

关于Exchanger,你可以把他看做一个中介,或者信使,它可以让两个运行的线程相互交换东西(Object),并且是带阻塞性质的。
打个比方,两个线程A,B两个要交换东西oa和ob,它们都在运行,使用exchanger这个中介,因为线程调度,并不知道那个线程先去到exchanger,这里假设为A。当A到了后,发现B还没来是吧,那它就要等待(park),当B来了后,发现B在exchanger那儿等它,他就和B交换oa和ob,并唤醒,然后它们两个线程就愉快的自己运行了。

当然上面只是并发量小的情况,如果一旦并发量大,则会使用多个中介(arena数组)来进行。

先给几个例子看看到底是怎么中介的。

例子

下面给出两个比较典型的例子讲解下具体意思:

例子1
public class ExchangerTest2 {
    private static volatile boolean isDone = false;

    static class ExchangerProducer implements Runnable {
        private Exchanger<Integer> exchanger;
        private static int data = 1;
        ExchangerProducer(Exchanger<Integer> exchanger) {
            this.exchanger = exchanger;
        }
        public void run() {
            try {
                data = 1;
                System.out.println("producer before: " + data);
                data = exchanger.exchange(data);
                System.out.println("producer after: " + data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    static class ExchangerConsumer implements Runnable {
        private Exchanger<Integer> exchanger;
        private static int data = 0;
        ExchangerConsumer(Exchanger<Integer> exchanger) {
            this.exchanger = exchanger;
        }
        public void run() {
            data = 0;
            System.out.println("consumer before : " + data);
            try {
                data = exchanger.exchange(data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("consumer after : " + data);
        }
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        Exchanger<Integer> exchanger = new Exchanger<Integer>();
        new Thread(new ExchangerConsumer(exchanger)).start();
        new Thread(new ExchangerProducer(exchanger)).start();
    }
}

输出为:
这里写图片描述

只有两个线程,并且两个线程只交换一次数据。consummer,然后consummer等待,producer提取。

例子2
public class ExchangerTest {

    static class Producer implements Runnable {
        private String buffer;
        private Exchanger<String> exchanger;
        Producer(String buffer, Exchanger<String> exchanger) {
            this.buffer = buffer;
            this.exchanger = exchanger;
        }
        public void run() {
            for (int i = 1; i < 5; i++) {
                try {
                    System.out.println("生产者第" + i + "次生产");
                    exchanger.exchange(buffer);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class Consumer implements Runnable {
        private String buffer;
        private final Exchanger<String> exchanger;
        public Consumer(String buffer, Exchanger<String> exchanger) {
            this.buffer = buffer;
            this.exchanger = exchanger;
        }
        public void run() {
            for (int i = 1; i < 5; i++) {
                // 调用exchange()与消费者进行数据交换
                try {
                    buffer = exchanger.exchange(buffer);
                    System.out.println("消费者第" + i + "次消费");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        String buffer1 = new String();
        String buffer2 = new String();
        Exchanger<String> exchanger = new Exchanger<String>();
        Thread producerThread = new Thread(new Producer(buffer1, exchanger));
        Thread consumerThread = new Thread(new Consumer(buffer2, exchanger));

        producerThread.start();
        consumerThread.start();
    }
}

输出:
这里写图片描述

对比第一个的输出,第二个例子的输出有点迷性,咋一看,生产者怎么不等消费者消费,就擅自第二次生产了?
其实没有,生产者还是在等待消费者的,只是由于cpu调度,消费者获取数据后,还没来得及消费,就又呗生产者抢到cpu时间,去进行第二次生产了。

现在估计有点感觉了吧

下面结合远吗具体分析。

形象的例子

学习代码时候,发现有大佬举过一个很形象的例子,可以帮助理解,这里就引用贴出来:

可以理解为多人之间,交换多个东西过程:

  1. 我先到一个叫做Slot的交易场所交易,发现你已经到了,那我就尝试喊你交易,如果你回应了我,决定和我交易那么进入第2步;如果别人抢先一步把你喊走了,那我就进入第5步。
  2. 我拿出钱交给你,你可能会接收我的钱,然后把货给我,交易结束;也可能嫌我掏钱太慢(超时)或者接个电话(中断),TM的不卖了,走了,那我只能再找别人买货了(从头开始)。
  3. 我到交易地点的时候,你不在,那我先尝试把这个交易点给占了(一屁股做凳子上…),如果我成功抢占了单间(交易点),那就坐这儿等着你拿货来交易,进入第4步;如果被别人抢座了,那我只能在找别的地方儿了,进入第5步。
  4. 你拿着货来了,喊我交易,然后完成交易;也可能我等了好长时间你都没来,我不等了,继续找别人交易去,走的时候我看了一眼,一共没多少人,弄了这么多单间(交易地点Slot),太TM浪费了,我喊来交易地点管理员:一共也没几个人,搞这么多单间儿干毛,给哥撤一个!。然后再找别人买货(从头开始);或者我老大给我打了个电话,不让我买货了(中断)。
  5. 我跑去喊管理员,尼玛,就一个坑交易个毛啊,然后管理在一个更加开阔的地方开辟了好多个单间,然后我就挨个来看每个单间是否有人。如果有人我就问他是否可以交易,如果回应了我,那我就进入第2步。如果我没有人,那我就占着这个单间等其他人来交易,进入第4步。
  6. 如果我尝试了几次都没有成功,我就会认为,是不是我TM选的这个单间风水不好?不行,得换个地儿继续(从头开始);如果我尝试了多次发现还没有成功,怒了,把管理员喊来:给哥再开一个单间(Slot),加一个凳子,这么多人就这么几个破凳子够谁用!

Exchanger实现原理分析

先来讲讲Exchanger里面一些重要属性。
首先里面的主要方法就只有两个:

  • public V exchange(V x) throws InterruptedException
  • public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException

即一个普通等待,一个超时时间控制的等待。

代码结构

其次,既然有线程等待,那么必然有数据结构,一个自定义的Node节点:

    @sun.misc.Contended static final class Node {
        /**
         * node在arena数组里面的索引
         */
        int index;              // Arena index 索引
        int bound;              // Last recorded value of Exchanger.bound 最后的exchanger的记录值。
        int collides;           // Number of CAS failures at current bound  如果CAS失败,就冲突自增
        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 当park时候,就把当前线程设置进去,否则为null。  
    }

前面讲过是交换数据嘛,所以需要一个Object来保存自己的资源,一个来保存自己获取的别人的资源。其他的字段可以看后面分析。

关于Contended 是防止伪共享的作用,具体可以看我这一片里面介绍:Java并发学习(十一)-LongAdder和LongAccumulator探究

还有一个值得注意的是里面有一个ThreadLocal变量:

/** 
     * ThreadLocal对象,里面放Node。
     * */
    static final class Participant extends ThreadLocal<Node> {
        public Node initialValue() { return new Node(); }
    }

    /**
     * 线程状态。
     */
    private final Participant participant;

所以每个线程虽然都使用Exchangeer,但是他们的participant并不相同,有各自的变量,这里关于ThreadLocal不多说。

接下来就是用于交换数据的slot和arena:

    /**
     * 当并发量大的时候,即多个线程用这一个Exchanger的时候
     */
    private volatile Node[] arena;

    /**
     * 开始用slot,并发量小的时候,直到冲突了就更改。
     */
    private volatile Node slot;
exchange方法

下面主要分析Exchanger方法:

    @SuppressWarnings("unchecked")
    public V exchange(V x) throws InterruptedException {
        Object v;
        Object item = (x == null) ? NULL_ITEM : x; // translate null args 判断x是否为null。
        //下面代码逻辑就是,当arena为null,就先尝试执行slotExchange方法,否则就执行arenaExchanger
        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(竞争不大)和arenaExchanger(竞争较大)这里主要分析这两个方法。

    /**
     * 当没有冲突不高的时候,也就是只有slot来交换数据的时候。
     */
    private final Object slotExchange(Object item, boolean timed, long ns) {
        Node p = participant.get();   //获取当前线程私有的node
        Thread t = Thread.currentThread();   //当前线程
        if (t.isInterrupted()) //   如果已经中断了。
            return null;

        for (Node q;;) {
            if ((q = slot) != null) {    //slot不为null时候,有人已经占了坑
                if (U.compareAndSwapObject(this, SLOT, q, null)) {  //null去替换q。也就是把这个slot置空,因为我来找你交换了啊,所以不用站这里了
                    Object v = q.item;   //记录相关slot里面线程所持有的数据。
                    q.match = item;    //我把你的也获取到。
                    Thread w = q.parked;  //交换完东西,唤醒你。
                    if (w != null)
                        U.unpark(w);
                    return v;
                }

                //如果走到这一步,就说明CAS失败了,判断是否需要用arena数组来支持。
                if (NCPU > 1 && bound == 0 &&      
                    U.compareAndSwapInt(this, BOUND, 0, SEQ))     //用SEQ去替换0
                    arena = new Node[(FULL + 2) << ASHIFT];     //初始化arena数组
            }
            else if (arena != null)
                return null; // caller must reroute to arenaExchange   //slot为null,但是arena不为空,那么就退出去执行arenaExchange方法。
            else {
                //slot为null,arena也为null,那么就说明现在没有线程到,当前线程是第一个到的,所以把p也就是threadLocal里面东西存到slot里面。
                p.item = item;
                if (U.compareAndSwapObject(this, SLOT, null, p))
                    break;
                p.item = null;
            }
        }

        // await release 等待去释放。
        int h = p.hash;
        long end = timed ? System.nanoTime() + ns : 0L;       //如果设定有超时获取时间。
        int spins = (NCPU > 1) ? SPINS : 1;     //设定自旋,如果是单核则次数为1
        Object v;
        while ((v = p.match) == null) {
            //p为当前线程的node,v即对方的资源为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();
            }
            else if (slot != p)
                //这个slot不是自己,被别人抢走了。
                spins = SPINS;
            else if (!t.isInterrupted() && arena == null &&
                     (!timed || (ns = end - System.nanoTime()) > 0L)) {
                //没有中断,且没有超时,那么你就park吧。
                //park过程。
                U.putObject(t, BLOCKER, this);
                p.parked = t;
                if (slot == p)
                    U.park(false, ns);
                p.parked = null;
                U.putObject(t, BLOCKER, null);
            }
            else if (U.compareAndSwapObject(this, SLOT, p, null)) {   
                //成功把slot置空,那么就跳出循环,此时要么返回超时,要么返回空。
                v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null;
                break;
            }
        }
        //CAS防止重排序法,把match设为null,因为什么也没拿到,拿到不会走着条路。
        U.putOrderedObject(p, MATCH, null);
        p.item = null;
        p.hash = h;
        return v;
    }

方法的核心就是竞争这个slot,如果slot里面有node了,那么就尝试跟它交换;如果没有东西,那么就尝试自己占领那个节点等待,直到有线程来跟我交换并唤醒。

arenaExchange方法

接下来看arenaExchange方法:

    /**
     * 当是启用了arenas的时候,的更换方法。保存above。
     * 也就是并发大时候,把slot换为数组操作。
     */
    private final Object arenaExchange(Object item, boolean timed, long ns) {
        Node[] a = arena;   //本地获取arena
        Node p = participant.get();      //获取当前线程的node节点。
        for (int i = p.index;;) {                      // 获得p在arena的索引
            int b, m, c; long j;                       //j是偏移量
            Node q = (Node)U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE);   //CAS方式从数组a里面获取q
            if (q != null && U.compareAndSwapObject(a, j, q, null)) {         //q不为null,就去跟它交换,并且置null
                Object v = q.item;                     // 获取它的item
                q.match = item;                   //把自己的item给他
                Thread w = q.parked;               //获取w并且唤醒它。
                if (w != null)
                    U.unpark(w);
                return v;
            }
            else if (i <= (m = (b = bound) & MMASK) && q == null) {
                //q为null,就说明这个位置没人,我就占这儿。
                p.item = item;                         // 自己要等待嘛,所以把自己的node节点的item,放入传入的item
                if (U.compareAndSwapObject(a, j, null, p)) {       //CAS方式,把p更换null。即尝试去占坑
                    long end = (timed && m == 0) ? System.nanoTime() + ns : 0L;   //如果有,获取end时间
                    Thread t = Thread.currentThread(); // wait  获取当前线程
                    for (int h = p.hash, spins = SPINS;;) {  //自旋操作
                        Object v = p.match;
                        if (v != null) {             
                            //p的match不为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();        // 睡眠一会
                        }
                        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); // 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)) {
                            //当前位置j仍然是p,并且成功把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;        // 减半,
                            if (Thread.interrupted())
                                return null;
                            if (timed && m == 0 && ns <= 0L)  //超时返回空
                                return TIMED_OUT;
                            break;                     // expired; restart 重新开始
                        }
                    }
                }
                else
                    p.item = null;                     // 没有占坑成功,那么就不换。
            }
            else {
                //需要的这个index,有人
                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
                    i = m + 1;                         // grow
                p.index = i;
            }
        }
    }

arenaExchange方法核心则相对slotExchanger复杂些,因为有了竞争,导致会CAS失败,所以这个时候要多准备几个slot就是arena数组。整个过程怎么理解呢?
可以理解为多个人之间换多个东西。所以要准备arena数组,否则众多线程等待,那会很影响性能的。
相互学习~

参考资料:
1. http://blog.csdn.net/chenssy/article/details/72550933
2. http://brokendreams.iteye.com/blog/2253956

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值