Netty中Queue的实现

概述

最近在看Netty的源码,关注了下其队列的实现;Netty中基于不同的IO模型,提供了不同的线程实现:

  1. BIO:ThreadPerChannelEventLoop
    每个Channel一个线程,采用的队列为LinkedBlockingQueue
  2. NIO:NioEventLoop(水平触发)
    每个线程一个Selector,可以注册多个Channel,采用的队列为MpscChunkedArrayQueue或MpscLinkedAtomicQueue
  3. Epoll:EpollEventLoop(边缘触发)
    和2相同
    那为什么要采用不同的Queue实现呢?下面看看不同Queue的具体实现;

LinkedBlockingQueue

LinkedBlockingQueue是JDK提供的,采用链表存储数据,通过ReentrantLock和Condition来解决竞争和支持堵塞;

既然采用链表,铁定要定义一个新的节点类,在LinkedBlockingQueue中这个节点类为:

static class Node<E> {
    E item;
    Node<E> next;

    Node(E x) { item = x;}
}

可以看到实现很简单,采用单向链接,通过next指向下一个节点,如果next为null,表示该节点为尾节点;

LinkedBlockingQueue的成员变量为:

//容量,队列是和ArrayList不同,有容量限制
private final int capacity;
//当前节点数量
private final AtomicInteger count = new AtomicInteger(0);
//头节点
private transient Node<E> head;
//尾节点
private transient Node<E> last;
//出列锁,当从队列取数据时,要先获取该锁
private final ReentrantLock takeLock = new ReentrantLock();
//队列非空条件变量,当队列为空时,出列线程要等待该条件变量
private final Condition notEmpty = takeLock.newCondition();
//入列锁,当往队列添加数据时,要先获取该锁
private final ReentrantLock putLock = new ReentrantLock();
//队列容量未满条件变量,当队列满了,入列线程要等待该条件变量
private final Condition notFull = putLock.newCondition();

从上面的成员变量大概可以看出:

  1. 可以设置容量,但未提供初始容量、最大容量之类的特性;
  2. 先入先出队列,入列和出列都要获取锁,因此是线程安全的;
  3. 入列和出列分为两个锁;

以其中的入列offer方法为例(由于netty中使用的是Queue而不是BlockingQueue,因此此处分析的都是非堵塞的方法):

public boolean offer(E e) {
    if (e == null) throw new NullPointerException();//参数非空
    final AtomicInteger count = this.count;//队列元素数量
    if (count.get() == capacity)//队列已满,无法添加,返回false
        return false;
    int c = -1;
    Node<E> node = new Node(e);//将元素封装为节点
    final ReentrantLock putLock = this.putLock;
    putLock.lock();//获取锁,所有入列操作共有同一个锁
    try {
        if (count.get() < capacity) {//只有队列不满,才能添加
            enqueue(node);//入列
            c = count.getAndIncrement();
            if (c + 1 < capacity)//如果添加元素之后,队列仍然不满,notFull条件变量满足条件,通知排队等待的线程
                notFull.signal();
        }
    } finally {
        putLock.unlock();//释放锁
    }
    if (c == 0)
        signalNotEmpty();//说明之前队列为空,因此需要出发非空条件变量
    return c >= 0;
}

ArrayBlockingQueue

顾名思义,ArrayBlockingQueue是采用数组存储数据的;它的成员变量如下:

//数组,用于存储数据
final Object[] items;
//ArrayBlockingQueue维护了两个索引,一个用于出列,一个用于入列
int takeIndex;
int putIndex;
//当前队列的元素数量
int count;
//可重入锁
final ReentrantLock lock;
//队列容量非空条件变量,当队列空了,出列线程要等待该条件变量
private final Condition notEmpty;
//队列容量未满条件变量,当队列满了,入列线程要等待该条件变量
private final Condition notFull;

从上面可出:

  1. 入列和出列采用同一个锁,也就是说入列和出列会彼此竞争锁;
  2. 采用索引来记录当前出列和入列的位置,避免了移动数组元素;
  3. 基于以上2点,在高并发的情况下,由于锁竞争,性能应该比不上链表的实现;

MpscChunkedArrayQueue

MpscChunkedArrayQueue也是采用数组来实现的,从名字上可以看出它是支持多生产者单消费者( Multi Producer Single Consumer),和前面的两种队列使用场景有些差异;但恰好符合netty的使用场景;它对特定场景进行了优化:

  1. CacheLine Padding
    LinkedBlockingQueue的head和last是相邻的,ArrayBlockingQueue的takeIndex和putIndex是相邻的;而我们都知道CPU将数据加载到缓存实际上是按照缓存行加载的,因此可能出现明明没有修改last,但由于出列操作修改了head,导致整个缓存行失效,需要重新进行加载;
//此处我将多个类中的变量合并到了一起,便于查看
long p01, p02, p03, p04, p05, p06, p07;
long p10, p11, p12, p13, p14, p15, p16, p17;
protected long producerIndex;
long p01, p02, p03, p04, p05, p06, p07;
long p10, p11, p12, p13, p14, p15, p16, p17;
protected long maxQueueCapacity;
protected long producerMask;
protected E[] producerBuffer;
protected volatile long producerLimit;
protected boolean isFixedChunkSize = false;
long p0, p1, p2, p3, p4, p5, p6, p7;
long p10, p11, p12, p13, p14, p15, p16, p17;
protected long consumerMask;
protected E[] consumerBuffer;
protected long consumerIndex;

可以看到生产者索引和消费者索引中间padding了18个long变量,18*8=144,而一般操作系统的cacheline为64,可以通过如下方式查看缓存行大小:

cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
  1. 减少锁的使用,使用CAS+自旋:
    由于使用锁会造成线程切换,消耗资源;因此MpscChunkedArrayQueue并未使用锁,而是使用自旋;和Disruptor的BusySpinWaitStrategy比较类似,如果系统比较繁忙,自旋效率会很适合;当然它也会造成CPU使用率比较高,所以建议使用时将这些线程绑定到特定的CPU;

  2. 支持扩容;
    MpscChunkedArrayQueue采用数组作为内部存储结构,那么它是如何实现扩容的呢?可能大家第一反应想到的是创建新数组,然后将老数据挪到新数组中去;但MpscChunkedArrayQueue采用了一种独特的方式,避免了数组的复制;
    举例说明:
    假设队列的初始化大小为4,则初始的buffer数组为4+1;为什么要+1呢?因为最后一个元素需要存储下一个buffer的指针;假设队列中存储了8个元素,则数组的内容如下:

  • buffer
数组下标 0 1 2 3 4
内容 e0 e1 e2 JUMP next[5]
  • next
数组下标 5 6 7 8 9
内容 e4 e5 JUMP e3 next

可以看到,每个buffer数组的大小都是固定的(之前的版本支持固定大小和非固定大小),也就是initialCapacity指定的大小;每个数组的最后一个实际保存的是个指针,指向下一个数组;读取数据时,如果遇到JUMP表示要从下一个buffer数组读取数据;

public E poll() {//消费队列元素
    final E[] buffer = consumerBuffer;
    final long index = consumerIndex;
    final long mask = consumerMask;
    //通过Unsafe.getObjectVolatile(E[] buffer, long offset)获取数组元素
    //因此需要根据数组索引,计算出在内存中的偏移量
    final long offset = modifiedCalcElementOffset(index, mask);
    Object e = lvElement(buffer, offset);
    if (e == null) {
        //e==null并不一定表示队列为空,因为入列的时候是先更新producerIndex,后更新数组元素,因此需要判断producerIndex
        if (index != lvProducerIndex()) {
            //采用自旋,直到获取到数据
            do {
                e = lvElement(buffer, offset);
            } while (e == null);
        }
        else {
            return null;
        }
    }
    if (e == JUMP) {//跳转到新的buff寻找
        final E[] nextBuffer = getNextBuffer(buffer, mask);
        return newBufferPoll(nextBuffer, index);
    }
    //从队列中取出数据之后,将数组对应位置元素清除
    soElement(buffer, offset, null);
    soConsumerIndex(index + 2);
    return (E) e;
}

性能对比

从网上找了一份测试代码,稍做修改:

public class TestQueue {
    private static int PRD_THREAD_NUM;
    private static int C_THREAD_NUM=1;

    private static int N = 1<<20;
    private static ExecutorService executor;

    public static void main(String[] args) throws Exception {
        System.out.println("Producer\tConsumer\tcapacity \t LinkedBlockingQueue \t ArrayBlockingQueue \t MpscLinkedAtomicQueue \t MpscChunkedArrayQueue \t MpscArrayQueue");

        for (int j = 1; j < 8; j++) {
            PRD_THREAD_NUM = (int) Math.pow(2, j);
            executor = Executors.newFixedThreadPool(PRD_THREAD_NUM * 2);

            for (int i = 9; i < 12; i++) {
                int length = 1<< i;
                System.out.print(PRD_THREAD_NUM + "\t\t");
                System.out.print(C_THREAD_NUM + "\t\t");
                System.out.print(length + "\t\t");
                System.out.print(doTest2(new LinkedBlockingQueue<Integer>(length), N) + "/s\t\t");
                System.out.print(doTest2(new ArrayBlockingQueue<Integer>(length), N) + "/s\t\t");
                System.out.print(doTest2(new MpscLinkedAtomicQueue<Integer>(), N) + "/s\t\t");
                System.out.print(doTest2(new MpscChunkedArrayQueue<Integer>(length), N) + "/s\t\t");
                System.out.print(doTest2(new MpscArrayQueue<Integer>(length), N) + "/s");
                System.out.println();
            }

            executor.shutdown();
        }
    }

    private static class Producer implements Runnable {
        int n;
        Queue<Integer> q;

        public Producer(int initN, Queue<Integer> initQ) {
            n = initN;
            q = initQ;
        }

        public void run() {
            while (n > 0) {
                if (q.offer(n)) {
                    n--;
                }
            }
        }
    }

    private static class Consumer implements Callable<Long> {
        int n;
        Queue<Integer> q;

        public Consumer(int initN, Queue<Integer> initQ) {
            n = initN;
            q = initQ;
        }

        public Long call() {
            long sum = 0;
            Integer e = null;
            while (n > 0) {
                if ((e = q.poll()) != null) {
                    sum += e;
                    n--;
                }

            }
            return sum;
        }
    }

    private static long doTest2(final Queue<Integer> q, final int n)
            throws Exception {
        CompletionService<Long> completionServ = new ExecutorCompletionService<>(executor);

        long t = System.nanoTime();
        for (int i = 0; i < PRD_THREAD_NUM; i++) {
            executor.submit(new Producer(n / PRD_THREAD_NUM, q));
        }
        for (int i = 0; i < C_THREAD_NUM; i++) {
            completionServ.submit(new Consumer(n / C_THREAD_NUM, q));
        }

        for (int i = 0; i < 1; i++) {
            completionServ.take().get();
        }

        t = System.nanoTime() - t;
        return (long) (1000000000.0 * N / t); // Throughput, items/sec
    }
}

chart.png

从上面可以看到:

  1. Mpsc*Queue表现最好,而且性能表现也最稳定;
  2. 并发数较低的时候,基于数组的队列比基于链表的队列表现要好,,推测有可能是因为数组在内存中是连续分配的,因此加载的时候可以有效利用缓存行,减少读的次数;而链表在内存的地址不是连续的,随机读代价比较大;
  3. 并发数较高的时候,基于链表的队列比基于数组的队列表现要好;LinkedBlockingQueue因为入列和出列采用不同的锁,因此锁竞争应该比ArrayBlockingQueue小;而MpscLinkedAtomicQueue没有容量限制,使用AtomicReference提供的XCHG功能修改链接即可达到出列和入列的目的,效率特别高;
  4. MpscChunkedArrayQueue相对于MpscArrayQueue,提供了动态扩容大能力;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容