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

来源:互联网 发布:飞控算法 编辑:程序博客网 时间:2024/05/17 22:37

断断续续看了一个多礼拜,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

原创粉丝点击