Chapter 11 浅析Exchanger类

来源:互联网 发布:网络流行词汇有哪些 编辑:程序博客网 时间:2024/06/06 17:30

1 概述

Exchanger类是java的j.u.c包中的同步辅助类,它特定作用于两个线程。这两个线程可以交换对象的同步点,在进入exchange方法时线程给出某个对象,并接受另外一个线程返回时给出的对象。和CyclicBarrier类相似的是,它们都是用来同步线程,使其动作的节奏一致,即都干完了某些事才能干下一件事。但是不同的是,Exchanger类只作用于两个线程,并且在调用阻塞方法exchange时,会有对象的给出与获得操作。

2 典型

Exchanger适用于同步两个线程,其经典的应用可以用警匪片中的吸毒者和毒贩子之间的交易为例来进行解释。吸毒者和毒贩子到指定的交易地点进行交易,当其中某个人提前到达交易地点时必须等待交易的另外一方到来。当两者都抵达交易地点后,两个人“一手交钱,一手交货”,吸毒者交出现金得到毒品,而毒贩子交出毒品得到现金。当两个人都得到自己想要的东西时,交易完成。

上述描述的场景抽象成代码如下:

import java.util.Random;import java.util.concurrent.Exchanger;/** * Created by fubinhe on 16/11/20. */public class ExchangerDemo {    public static void main(String[] args) {        final Exchanger<String> exchanger = new Exchanger<>();        final String[] ss = {"10克冰毒", "1万美金"};        Thread[] threads = new Thread[2];        for (int i = 0; i < 2; ++i) {            final int index = i;            threads[i] = new Thread(new Runnable() {                @Override                public void run() {                    System.out.println(Thread.currentThread().getName() + "正准备把" + ss[index] + "交换出去......");                    try {                        Thread.sleep(new Random().nextInt(2000));                        String str = exchanger.exchange(ss[index]);                        System.out.println(Thread.currentThread().getName() + "交换到" + str);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            });            threads[i].start();        }        for (int i = 0; i < 2; ++i) {            try {                threads[i].join();            } catch (InterruptedException e) {                e.printStackTrace();            }        }        System.out.println("交易完成");    }}

上述程序的运行结果如下,其中Thread-0可以看成毒贩子,而Thread-1可以看成吸毒者。注意,在上述代码中,在主线程中对两个子线程都调用了join方法,这是为了让主线程等待这两个子线程都运行完之后才能宣告交易完成。其实还可以用yield函数来实现,即31-37行的代码可以用下列代码来实现。

while (threads[0].isAlive() || threads[1].isAlive()) {    Thread.yield();}

3 源码浅析

3.1 exchange函数

Exchanger类的最主要的函数就是exchange函数,其主要功能是叫己方线程中的缓冲区数据交换对方缓冲区中的数据,其源代码如下:

public V exchange(V x) throws InterruptedException {    if (!Thread.interrupted()) {        Object v = doExchange((x == null) ? NULL_ITEM : x, false, 0);        if (v == NULL_ITEM)            return null;        if (v != CANCEL)            return (V)v;        Thread.interrupted(); // Clear interrupt status on IE throw    }    throw new InterruptedException();}

可以看到,其调用了真正干活的doExchange函数,该函数源代码如下:

private Object doExchange(Object item, boolean timed, long nanos) {    Node me = new Node(item);                 // Create in case occupying    int index = hashIndex();                  // Index of current slot    int fails = 0;                            // Number of CAS failures    for (;;) {        Object y;                             // Contents of current slot        Slot slot = arena[index];        if (slot == null)                     // Lazily initialize slots            createSlot(index);                // Continue loop to reread        else if ((y = slot.get()) != null &&  // Try to fulfill                 slot.compareAndSet(y, null)) {            Node you = (Node)y;               // Transfer item            if (you.compareAndSet(null, item)) {                LockSupport.unpark(you.waiter);                return you.item;            }                                 // Else cancelled; continue        }        else if (y == null &&                 // Try to occupy                 slot.compareAndSet(null, me)) {            if (index == 0)                   // Blocking wait for slot 0                return timed ?                    awaitNanos(me, slot, nanos) :                    await(me, slot);            Object v = spinWait(me, slot);    // Spin wait for non-0            if (v != CANCEL)                return v;            me = new Node(item);              // Throw away cancelled node            int m = max.get();            if (m > (index >>>= 1))           // Decrease index                max.compareAndSet(m, m - 1);  // Maybe shrink table        }        else if (++fails > 1) {               // Allow 2 fails on 1st slot            int m = max.get();            if (fails > 3 && m < FULL && max.compareAndSet(m, m + 1))                index = m + 1;                // Grow on 3rd failed slot            else if (--index < 0)                index = m;                    // Circularly traverse        }    }}

要理解上述的源码,需要理解两个核心技术,即CacheLine填充技术和锁分离

3.2 CacheLine填充技术

在上述代码中可以看到有一个成员变量arena,它是一个Slot类型的数组,而Slot是一个内部类

private static final class Slot extends AtomicReference<Object> {    // Improve likelihood of isolation on <= 64 byte cache lines    long q0, q1, q2, q3, q4, q5, q6, q7, q8, q9, qa, qb, qc, qd, qe;}private volatile Slot[] arena = new Slot[CAPACITY];

可以看到Slot继承自AtomicReference,并维护了q0, q1...qe的变量,而这些变量是多余的。那么为什么要使用这些多余的变量呢?

网上的说法是为了让不同的Slot不要落在cpu的同一个CacheLine里面。当CPU从内存读取数据的时候,不是一个字节一个字节的读,而是按块读取,这里的块也就是CacheLine,一般一个CacheLine大小是64Byte。使用这些多余的变量是为了保证一个Slot的大小 >= 64Byte,这样更改一个Slot,就不会导致另外一个Slot的CPU缓存失效,从而提高性能。但是为什么是15个long类型的变量,其原因并不是特别理解,因为15个long类型变量占用的空间远远大于64Byte。

3.3 锁分离

为了提高并发的性能,Exchanger类定义了多个Slot(其实是CAPACITY=32个Slot),这样并发调用exchange方法的时候,就可以分散在不同的Slot里面进行交换,这一点与ConcurrentHashMap的锁分离技术类似。

参考doExchange函数,程序的基本流程如下:

step 1. 利用hashIndex函数,根据线程的id计算出所在的Slot的index,定位到需要进行交换的Slot

step 2. 如果当前的Slot为null(area数组使用了延迟加载),则利用createSlot函数初始化该Slot并继续循环

step 3. 如果该Slot其它线程占着(Slot里面有node)并正在等待交换,那就和它进行交换

step 4. 如果该Slot里面没有node,自己占着并等待交换。如果没有线程前来交换,向前挪个位置,把当前Slot里面内容取消,index减半,直到有线程来交换 

step 5. 最终挪到0位置,如果还没有线程来交换,则一直阻塞

step 6. 在上述过程中如果出现失败(失败的情况包括:Slot中没有node时抢占Slot失败,或者Slot中有node时争取交换失败)的次数大于2时,扩大index继续循环

4 总结

在JDK 1.5时,Exchanger类还是使用容量为1的容器,到了JDK 1.6时,借用了ConcurrentHashMap的锁分离技术,同时为了提高并发性能,使用多余变量使得两个不同的Slot不会占用同一个CacheLine。




0 0
原创粉丝点击