原理剖析(第 012 篇)Netty之无锁队列MpscUnboundedArrayQueue原理分析

-

一、大致介绍

1、了解过netty原理的童鞋,其实应该知道工作线程组的每个子线程都维护了一个任务队列;
2、细心的童鞋会发现netty的队列是重写了队列的实现方法,覆盖了父类中的LinkedBlockingQueue队列,但是如今却换成了JCTools的一些并发队列,因为JCTools是一款对jdk并发数据结构进行增强的并发工具;
3、那么问题就来了,现在的netty要用新的队列呢?难道是新的队列确实很高效么?
4、那么本章节就来和大家分享分析一下Netty新采用的队列之一MpscUnboundedArrayQueue,分析Netty的源码版本为:netty-netty-4.1.22.Final;

二、回顾预习

2.1 构造队列

1、源码:// NioEventLoop.java@Overrideprotected Queue<Runnable> newTaskQueue(int maxPendingTasks) {// This event loop never calls takeTask()// 由于默认是没有配置io.netty.eventLoop.maxPendingTasks属性值的,所以maxPendingTasks默认值为Integer.MAX_VALUE;// 那么最后配备的任务队列的大小也就自然使用无参构造队列方法return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue(): PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);}// PlatformDependent.java/*** Create a new {@link Queue} which is safe to use for multiple producers (different threads) and a single* consumer (one thread!).* @return A MPSC queue which may be unbounded.*/public static <T> Queue<T> newMpscQueue() {return Mpsc.newMpscQueue();}// Mpsc.javastatic <T> Queue<T> newMpscQueue() {return USE_MPSC_CHUNKED_ARRAY_QUEUE ? new MpscUnboundedArrayQueue<T>(MPSC_CHUNK_SIZE): new MpscUnboundedAtomicArrayQueue<T>(MPSC_CHUNK_SIZE);}   2、通过源码回顾,想必大家已经隐约回忆起之前分析过这段代码,我们在构建工作线程管理组的时候,还需要实例化子线程数组children[],所以自然就会碰到这段代码;3、这段代码其实就是为了实现一个无锁方式的线程安全队列,总之一句话,效率相当相当的高;

2.2 何为JCTools?

1、JCTools是服务虚拟机并发开发的工具,提供一些JDK没有的并发数据结构辅助开发。2、是一个聚合四种 SPSC/MPSC/SPMC/MPMC 数据变量的并发队列:• SPSC:单个生产者对单个消费者(无等待、有界和无界都有实现)• MPSC:多个生产者对单个消费者(无锁、有界和无界都有实现)• SPMC:单生产者对多个消费者(无锁 有界)• MPMC:多生产者对多个消费者(无锁、有界)3、SPSC/MPSC 提供了一个在性能,分配和分配规则之间的平衡的关联数组队列;

2.3 常用重要的成员属性及方法

1、private volatile long producerLimit;// 数据链表所分配或者扩展后的容量值2、protected long producerIndex;// 生产者指针,每添加一个数据,指针加23、protected long consumerIndex;// 消费者指针,每移除一个数据,指针加24、private static final int RETRY = 1; // 重新尝试,有可能是因为并发原因,CAS操作指针失败,所以需要重新尝试添加动作private static final int QUEUE_FULL = 2; // 队列已满,直接返回false操作private static final int QUEUE_RESIZE = 3; // 需要扩容处理,扩容的后的容量值producerLimit一般都是mask的N倍// 添加数据时,根据offerSlowPath返回的状态值来做各种处理5、protected E[] producerBuffer;// 数据缓冲区,需要添加的数据放在此6、protected long producerMask;// 生产者扩充容量值,一般producerMask与consumerMask是一致的,而且需要扩容的数值一般和此值一样7、public boolean offer(final E e)// 添加元素8、public E poll()// 移除元素

2.4 数据结构

1、如果chunkSize初始化大小为4,则最后显示的数据结构如下:
   E1,E2,。。。,EN:表示具体的元素;
   NBP:表示下一个缓冲区的指针,我采用的是英文的缩写( Next Buffer Pointer);   而且你看着我是拆分开写的,其实每一个NBP指向的就是下面一组缓冲区;
   Buffer1中的NBP其实就是Buffer2的指针引用;
   Buffer2中的NBP其实就是Buffer3的指针引用;
   以此类推。。。
+------+------+------+------+------+
|      |      |      |      |      |
|  E1  |  E2  |  E3  | JUMP |  NBP |    Buffer1
|      |      |      |      |      |
+------+------+------+------+------++------+------+------+------+------+
|      |      |      |      |      |
|  E5  |  E6  | JUMP |  E4  |  NBP |    Buffer2
|      |      |      |      |      |
+------+------+------+------+------++------+------+------+------+------+
|      |      |      |      |      |
|  E9  | JUMP |  E7  |  E8  |  NBP |    Buffer3
|      |      |      |      |      |
+------+------+------+------+------++------+------+------+------+------+
|      |      |      |      |      |
| JUMP |  E10 |  E11 |  E12 |  NBP |    Buffer4
|      |      |      |      |      |
+------+------+------+------+------++------+------+------+------+------+
|      |      |      |      |      |
|  E13 |  E14 |  E15 | JUMP |  NBP |    Buffer5
|      |      |      |      |      |
+------+------+------+------+------+2、这个数据结构和我们通常所认知的链表是不是有点异样,其实大体还是雷同的,这种数据结构其实也是指针的单项指引罢了;

三、源码分析MpscUnboundedArrayQueue

3.1、MpscUnboundedArrayQueue(int)

1、源码:// MpscUnboundedArrayQueue.javapublic MpscUnboundedArrayQueue(int chunkSize){super(chunkSize); // 调用父类的含参构造方法}// BaseMpscLinkedArrayQueue.java/*** @param initialCapacity the queue initial capacity. If chunk size is fixed this will be the chunk size.*                        Must be 2 or more.*/public BaseMpscLinkedArrayQueue(final int initialCapacity){// 校验队列容量值,大小必须不小于2RangeUtil.checkGreaterThanOrEqual(initialCapacity, 2, "initialCapacity");// 通过传入的参数通过Pow2算法获取大于initialCapacity最近的一个2的n次方的值int p2capacity = Pow2.roundToPowerOfTwo(initialCapacity);// leave lower bit of mask clearlong mask = (p2capacity - 1) << 1; // 通过p2capacity计算获得mask值,该值后续将用作扩容的值// need extra element to point at next arrayE[] buffer = allocate(p2capacity + 1); // 默认分配一个 p2capacity + 1 大小的数据缓冲区producerBuffer = buffer;producerMask = mask;consumerBuffer = buffer;consumerMask = mask;// 同时用mask作为初始化队列的Limit值,当生产者指针producerIndex超过该Limit值时就需要做扩容处理soProducerLimit(mask); // we know it's all empty to start with}// RangeUtil.javapublic static int checkGreaterThanOrEqual(int n, int expected, String name){// 要求队列的容量值必须不小于 expected 值,这个 expected 值由上层决定,但是对 MpscUnboundedArrayQueue 而言,expected 为 2;// 那么就是说 MpscUnboundedArrayQueue 的值必须不小于 2;if (n < expected){throw new IllegalArgumentException(name + ": " + n + " (expected: >= " + expected + ')');}return n;}2、通过调用父类的构造方法,分配了一个数据缓冲区,初始化容量大小,并且容量值不小于2,差不多就这样队列的实例化操作已经完成了;

3.2、offer(E)

1、源码:// BaseMpscLinkedArrayQueue.java@Overridepublic boolean offer(final E e){if (null == e) // 待添加的元素e不允许为空,否则抛空指针异常{throw new NullPointerException();}long mask;E[] buffer;long pIndex;while (true){long producerLimit = lvProducerLimit(); // 获取当前数据Limit的阈值pIndex = lvProducerIndex(); // 获取当前生产者指针位置// lower bit is indicative of resize, if we see it we spin until it's clearedif ((pIndex & 1) == 1){continue;}// pIndex is even (lower bit is 0) -> actual index is (pIndex >> 1)// mask/buffer may get changed by resizing -> only use for array access after successful CAS.mask = this.producerMask;buffer = this.producerBuffer;// a successful CAS ties the ordering, lv(pIndex) - [mask/buffer] -> cas(pIndex)// assumption behind this optimization is that queue is almost always empty or near emptyif (producerLimit <= pIndex) // 当阈值小于等于生产者指针位置时,则需要扩容,否则直接通过CAS操作对pIndex做加2处理{// 通过offerSlowPath返回状态值,来查看怎么来处理这个待添加的元素int result = offerSlowPath(mask, pIndex, producerLimit);switch (result){case CONTINUE_TO_P_INDEX_CAS:break;case RETRY: // 可能由于并发原因导致CAS失败,那么则再次重新尝试添加元素continue;case QUEUE_FULL: // 队列已满,直接返回false操作return false;case QUEUE_RESIZE: // 队列需要扩容操作resize(mask, buffer, pIndex, e); // 对队列进行直接扩容操作return true;}}// 能走到这里,则说明当前的生产者指针位置还没有超过阈值,因此直接通过CAS操作做加2处理if (casProducerIndex(pIndex, pIndex + 2)) {break;}}// INDEX visible before ELEMENT// 获取计算需要添加元素的位置final long offset = modifiedCalcElementOffset(pIndex, mask);// 在buffer的offset位置添加e元素soElement(buffer, offset, e); // release element ereturn true;}// BaseMpscLinkedArrayQueueProducerFields.java@Overridepublic final long lvProducerIndex(){// 通过Unsafe对象调用native方法,获取生产者指针位置return UNSAFE.getLongVolatile(this, P_INDEX_OFFSET);}   // UnsafeRefArrayAccess.java/*** An ordered store(store + StoreStore barrier) of an element to a given offset** @param buffer this.buffer* @param offset computed via {@link UnsafeRefArrayAccess#calcElementOffset}* @param e      an orderly kitty*/public static <E> void soElement(E[] buffer, long offset, E e){// 通过Unsafe对象调用native方法,将元素e设置到buffer缓冲区的offset位置UNSAFE.putOrderedObject(buffer, offset, e);}2、此方法为添加新的元素对象,当pIndex指针超过阈值producerLimit时则扩容处理,否则直接通过CAS操作添加记录pIndex位置;

3.3、offerSlowPath(long, long, long)

1、源码:// BaseMpscLinkedArrayQueue.java/*** We do not inline resize into this method because we do not resize on fill.*/private int offerSlowPath(long mask, long pIndex, long producerLimit){// 获取消费者指针final long cIndex = lvConsumerIndex();// 获取当前缓冲区的容量值,getCurrentBufferCapacity方法由子类MpscUnboundedArrayQueue实现,默认返回mask值long bufferCapacity = getCurrentBufferCapacity(mask);// 如果消费指针加上容量值如果超过了生产指针,那么则会尝试进行扩容处理if (cIndex + bufferCapacity > pIndex){if (!casProducerLimit(producerLimit, cIndex + bufferCapacity)){// retry from topreturn RETRY;}else{// continue to pIndex CASreturn CONTINUE_TO_P_INDEX_CAS;}}// full and cannot grow 子类MpscUnboundedArrayQueue默认返回Integer.MAX_VALUE值,所以不会进入此分支else if (availableInQueue(pIndex, cIndex) <= 0){// offer should return false;return QUEUE_FULL;}// grab index for resize -> set lower bit 尝试扩容队列else if (casProducerIndex(pIndex, pIndex + 1)){// trigger a resizereturn QUEUE_RESIZE;}else{// failed resize attempt, retry from topreturn RETRY;}}// MpscUnboundedArrayQueue.java@Overrideprotected long getCurrentBufferCapacity(long mask){// 获取当前缓冲区的容量值return mask;}   // BaseMpscLinkedArrayQueue.javafinal boolean casProducerLimit(long expect, long newValue){// 通过CAS尝试对阈值进行修改扩容处理return UNSAFE.compareAndSwapLong(this, P_LIMIT_OFFSET, expect, newValue);}// MpscUnboundedArrayQueue.java@Overrideprotected long availableInQueue(long pIndex, long cIndex){// 获取可用容量值return Integer.MAX_VALUE;}   // BaseMpscLinkedArrayQueueProducerFields.javafinal boolean casProducerIndex(long expect, long newValue){// 通过CAS操作更新生产者指针return UNSAFE.compareAndSwapLong(this, P_INDEX_OFFSET, expect, newValue);}   2、该方法主要通过一系列的if...else判断,并结合子类MpscUnboundedArrayQueue的一些重写方法来判断针对该新添加的元素要做何种状态处理;

3.4、resize(long, E[], long, E)

1、源码:// BaseMpscLinkedArrayQueue.javaprivate void resize(long oldMask, E[] oldBuffer, long pIndex, E e){// 获取oldBuffer的长度值int newBufferLength = getNextBufferSize(oldBuffer);// 重新创建新的缓冲区final E[] newBuffer = allocate(newBufferLength);producerBuffer = newBuffer; // 将新创建的缓冲区赋值到生产者缓冲区对象上final int newMask = (newBufferLength - 2) << 1;producerMask = newMask;// 根据oldMask获取偏移位置值final long offsetInOld = modifiedCalcElementOffset(pIndex, oldMask);// 根据newMask获取偏移位置值final long offsetInNew = modifiedCalcElementOffset(pIndex, newMask);// 将元素e设置到新的缓冲区newBuffer的offsetInNew位置处soElement(newBuffer, offsetInNew, e);// element in new array// 通过nextArrayOffset(oldMask)计算新的缓冲区将要放置旧的缓冲区的哪个位置// 将新的缓冲区newBuffer设置到旧的缓冲区oldBuffer的nextArrayOffset(oldMask)位置处// 主要是将oldBuffer中最后一个元素的位置指向新的缓冲区newBuffer// 这样就构成了一个单向链表指向的关系soElement(oldBuffer, nextArrayOffset(oldMask), newBuffer);// buffer linked// ASSERT codefinal long cIndex = lvConsumerIndex();final long availableInQueue = availableInQueue(pIndex, cIndex);RangeUtil.checkPositive(availableInQueue, "availableInQueue");// Invalidate racing CASs// We never set the limit beyond the bounds of a buffer// 重新扩容阈值,因为availableInQueue反正都是Integer.MAX_VALUE值,所以自然就取mask值啦// 因此针对MpscUnboundedArrayQueue来说,扩容的值其实就是mask的值的大小soProducerLimit(pIndex + Math.min(newMask, availableInQueue));// make resize visible to the other producers// 设置生产者指针加2处理soProducerIndex(pIndex + 2);// INDEX visible before ELEMENT, consistent with consumer expectation// make resize visible to consumer// 用一个空对象来衔接新老缓冲区,凡是在缓冲区中碰到JUMP对象的话,那么就得琢磨着准备着获取下一个缓冲区的数据元素了soElement(oldBuffer, offsetInOld, JUMP);}// MpscUnboundedArrayQueue.java@Overrideprotected int getNextBufferSize(E[] buffer){// 获取buffer缓冲区的长度return length(buffer);}// LinkedArrayQueueUtil.javastatic int length(Object[] buf){// 直接通过length属性来获取数组的长度return buf.length;}   // CircularArrayOffsetCalculator.java@SuppressWarnings("unchecked")public static <E> E[] allocate(int capacity){// 根据容量值创建数组return (E[]) new Object[capacity];}   2、该方法主要完成新的元素的放置,同时也完成了扩容操作,采用单向链表指针关系,将原缓冲区和新创建的缓冲区衔接起来;

3.5、poll()

1、源码:// BaseMpscLinkedArrayQueue.java/*** {@inheritDoc}* <p>* This implementation is correct for single consumer thread use only.*/@SuppressWarnings("unchecked")@Overridepublic E poll(){final E[] buffer = consumerBuffer; // 获取缓冲区的数据final long index = consumerIndex;final long mask = consumerMask;// 根据消费指针与mask来获取当前需要从哪个位置开始来移除元素final long offset = modifiedCalcElementOffset(index, mask);// 从buffer缓冲区的offset位置获取元素内容Object e = lvElement(buffer, offset);// LoadLoadif (e == null) // 如果元素为null的话{// 则再探讨看看消费指针是不是和生产指针是不是相同if (index != lvProducerIndex()){// poll() == null iff queue is empty, null element is not strong enough indicator, so we must// check the producer index. If the queue is indeed not empty we spin until element is// visible.// 若不相同的话,则先尝试从buffer缓冲区的offset位置获取元素先,若获取元素为null则结束while处理do{e = lvElement(buffer, offset);}while (e == null);}// 说明消费指针是不是和生产指针是相等的,那么则缓冲区的数据已经被消费完了,直接返回null即可else{return null;}}// 如果元素为JUMP空对象的话,那么意味着我们就得获取下一缓冲区进行读取数据了if (e == JUMP){// final E[] nextBuffer = getNextBuffer(buffer, mask);// return newBufferPoll(nextBuffer, index);}// 能执行到这里,说明需要移除的元素既不是空的,也不是JUMP空对象,那么则就按照正常处理置空即可// 移除元素时,则将buffer缓冲区的offset位置的元素置为空即可soElement(buffer, offset, null); // release element null// 同时也通过CAS操作增加消费指针的关系,加2操作soConsumerIndex(index + 2); // release cIndexreturn (E) e;}// BaseMpscLinkedArrayQueueProducerFields.java@Overridepublic final long lvProducerIndex(){// 通过Unsafe对象调用native方法,获取当前生产者指针值return UNSAFE.getLongVolatile(this, P_INDEX_OFFSET);}   // UnsafeRefArrayAccess.java/*** A volatile load (load + LoadLoad barrier) of an element from a given offset.** @param buffer this.buffer* @param offset computed via {@link UnsafeRefArrayAccess#calcElementOffset(long)}* @return the element at the offset*/@SuppressWarnings("unchecked")public static <E> E lvElement(E[] buffer, long offset){// 通过Unsafe对象调用native方法,获取buffer缓冲区offset位置的元素return (E) UNSAFE.getObjectVolatile(buffer, offset);}// BaseMpscLinkedArrayQueue.java@SuppressWarnings("unchecked")private E[] getNextBuffer(final E[] buffer, final long mask){// 获取下一个缓冲区的偏移位置值final long offset = nextArrayOffset(mask);// 从buffer缓冲区的offset位置获取下一个缓冲区数组final E[] nextBuffer = (E[]) lvElement(buffer, offset);// 获取出来后,同时将buffer缓冲区的offset位置置为空,代表指针已经被取出,原来位置没用了,清空即可soElement(buffer, offset, null);return nextBuffer;}// BaseMpscLinkedArrayQueue.javaprivate E newBufferPoll(E[] nextBuffer, long index){// 从下一个新的缓冲区中找到需要移除数据的指针位置final long offset = newBufferAndOffset(nextBuffer, index);// 从newBuffer新的缓冲区中offset位置取出元素final E n = lvElement(nextBuffer, offset);// LoadLoadif (n == null) // 若取出的元素为空,则直接抛出异常{throw new IllegalStateException("new buffer must have at least one element");}// 如果取出的元素不为空,那么先将这个元素原先的位置内容先清空掉soElement(nextBuffer, offset, null);// StoreStore// 然后通过Unsafe对象调用native方法,修改消费指针的数值偏移加2处理soConsumerIndex(index + 2);return n;}   2、该方法主要阐述了该队列是如何的移除数据的;取出的数据如果为JUMP空对象的话,那么则准备从下一个缓冲区获取数据元素,否则还是从当前的缓冲区对象中移除元素,并且更新消费指针;

3.6、size()

1、源码:// BaseMpscLinkedArrayQueue.java@Overridepublic final int size(){// NOTE: because indices are on even numbers we cannot use the size util./** It is possible for a thread to be interrupted or reschedule between the read of the producer and* consumer indices, therefore protection is required to ensure size is within valid range. In the* event of concurrent polls/offers to this method the size is OVER estimated as we read consumer* index BEFORE the producer index.*/long after = lvConsumerIndex(); // 获取消费指针long size;while (true) // 为了防止在获取大小的时候指针发生变化,那么则死循环自旋方式获取大小数值{final long before = after;final long currentProducerIndex = lvProducerIndex(); // 获取生产者指针after = lvConsumerIndex(); // 获取消费指针// 如果后获取的消费指针after和之前获取的消费指针before相等的话,那么说明此刻还没有指针变化if (before == after){// 那么则直接通过生产指针直接减去消费指针,然后向偏移一位,即除以2,得出最后size大小size = ((currentProducerIndex - after) >> 1);// 计算完了之后则直接break中断处理break;}// 若消费指针前后不一致,那么可以说是由于并发原因导致了指针发生了变化;// 那么则进行下一次循环继续获取最新的指针值再次进行判断}// Long overflow is impossible, so size is always positive. Integer overflow is possible for the unbounded// indexed queues.if (size > Integer.MAX_VALUE){return Integer.MAX_VALUE;}else{return (int) size;}}2、获取缓冲区数据大小其实很简单,就是拿着生产指针减去消费指针,但是为了防止并发操作计算错,才用了死循环的方式计算zise值;

3.7、isEmpty()

1、源码:// BaseMpscLinkedArrayQueue.java@Overridepublic final boolean isEmpty(){// Order matters!// Loading consumer before producer allows for producer increments after consumer index is read.// This ensures this method is conservative in it's estimate. Note that as this is an MPMC there is// nothing we can do to make this an exact method.// 这个就简单了,直接判断消费指针和生产指针是不是相等就知道了return (this.lvConsumerIndex() == this.lvProducerIndex());}2、通过前面我们已经知道了,添加数据的话生产指针在不停的累加操作,而做移除数据的时候消费指针也在不停的累加操作;3、那么这种指针总会有一天会碰面的吧,碰面的那个时候则是数据已经空空如也的时刻;

四、性能测试

1、测试Demo:
/*** 比较队列的消耗情况。** @author hmilyylimh* ^_^* @version 0.0.1* ^_^* @date 2018/3/30*/
public class CompareQueueCosts {/** 生产者数量 */private static int COUNT_OF_PRODUCER = 2;/** 消费者数量 */private static final int COUNT_OF_CONSUMER = 1;/** 准备添加的任务数量值 */private static final int COUNT_OF_TASK = 1 << 20;/** 线程池对象 */private static ExecutorService executorService;public static void main(String[] args) throws Exception {for (int j = 1; j < 7; j++) {COUNT_OF_PRODUCER = (int) Math.pow(2, j);executorService = Executors.newFixedThreadPool(COUNT_OF_PRODUCER * 2);int basePow = 8;int capacity = 0;for (int i = 1; i <= 3; i++) {capacity = 1 << (basePow + i);System.out.print("Producers: " + COUNT_OF_PRODUCER + "\t\t");System.out.print("Consumers: " + COUNT_OF_CONSUMER + "\t\t");System.out.print("Capacity: " + capacity + "\t\t");System.out.print("LinkedBlockingQueue: " + testQueue(new LinkedBlockingQueue<Integer>(capacity), COUNT_OF_TASK) + "s" + "\t\t");// System.out.print("ArrayList: " + testQueue(new ArrayList<Integer>(capacity), COUNT_OF_TASK) + "s" + "\t\t");// System.out.print("LinkedList: " + testQueue(new LinkedList<Integer>(), COUNT_OF_TASK) + "s" + "\t\t");System.out.print("MpscUnboundedArrayQueue: " + testQueue(new MpscUnboundedArrayQueue<Integer>(capacity), COUNT_OF_TASK) + "s" + "\t\t");System.out.print("MpscChunkedArrayQueue: " + testQueue(new MpscChunkedArrayQueue<Integer>(capacity), COUNT_OF_TASK) + "s" + "\t\t");System.out.println();}System.out.println();executorService.shutdown();}}private static Double testQueue(final Collection<Integer> queue, final int taskCount) throws Exception {CompletionService<Long> completionService = new ExecutorCompletionService<Long>(executorService);long start = System.currentTimeMillis();for (int i = 0; i < COUNT_OF_PRODUCER; i++) {executorService.submit(new Producer(queue, taskCount / COUNT_OF_PRODUCER));}for (int i = 0; i < COUNT_OF_CONSUMER; i++) {completionService.submit((new Consumer(queue, taskCount / COUNT_OF_CONSUMER)));}for (int i = 0; i < COUNT_OF_CONSUMER; i++) {completionService.take().get();}long end = System.currentTimeMillis();return Double.parseDouble("" + (end - start)) / 1000;}private static class Producer implements Runnable {private Collection<Integer> queue;private int taskCount;public Producer(Collection<Integer> queue, int taskCount) {this.queue = queue;this.taskCount = taskCount;}@Overridepublic void run() {// Queue队列if (this.queue instanceof Queue) {Queue<Integer> tempQueue = (Queue<Integer>) this.queue;while (this.taskCount > 0) {if (tempQueue.offer(this.taskCount)) {this.taskCount--;} else {// System.out.println("Producer offer failed.");}}}// List列表else if (this.queue instanceof List) {List<Integer> tempList = (List<Integer>) this.queue;while (this.taskCount > 0) {if (tempList.add(this.taskCount)) {this.taskCount--;} else {// System.out.println("Producer offer failed.");}}}}}private static class Consumer implements Callable<Long> {private Collection<Integer> queue;private int taskCount;public Consumer(Collection<Integer> queue, int taskCount) {this.queue = queue;this.taskCount = taskCount;}@Overridepublic Long call() {// Queue队列if (this.queue instanceof Queue) {Queue<Integer> tempQueue = (Queue<Integer>) this.queue;while (this.taskCount > 0) {if ((tempQueue.poll()) != null) {this.taskCount--;}}}// List列表else if (this.queue instanceof List) {List<Integer> tempList = (List<Integer>) this.queue;while (this.taskCount > 0) {if (!tempList.isEmpty() && (tempList.remove(0)) != null) {this.taskCount--;}}}return 0L;}}
}2、指标结果:
Producers: 2        Consumers: 1        Capacity: 512       LinkedBlockingQueue: 1.399s     MpscUnboundedArrayQueue: 0.109s     MpscChunkedArrayQueue: 0.09s
Producers: 2        Consumers: 1        Capacity: 1024      LinkedBlockingQueue: 1.462s     MpscUnboundedArrayQueue: 0.041s     MpscChunkedArrayQueue: 0.048s
Producers: 2        Consumers: 1        Capacity: 2048      LinkedBlockingQueue: 0.281s     MpscUnboundedArrayQueue: 0.037s     MpscChunkedArrayQueue: 0.082s       Producers: 4        Consumers: 1        Capacity: 512       LinkedBlockingQueue: 0.681s     MpscUnboundedArrayQueue: 0.085s     MpscChunkedArrayQueue: 0.133s
Producers: 4        Consumers: 1        Capacity: 1024      LinkedBlockingQueue: 0.405s     MpscUnboundedArrayQueue: 0.094s     MpscChunkedArrayQueue: 0.172s
Producers: 4        Consumers: 1        Capacity: 2048      LinkedBlockingQueue: 0.248s     MpscUnboundedArrayQueue: 0.107s     MpscChunkedArrayQueue: 0.153s       Producers: 8        Consumers: 1        Capacity: 512       LinkedBlockingQueue: 1.523s     MpscUnboundedArrayQueue: 0.093s     MpscChunkedArrayQueue: 0.172s
Producers: 8        Consumers: 1        Capacity: 1024      LinkedBlockingQueue: 0.668s     MpscUnboundedArrayQueue: 0.094s     MpscChunkedArrayQueue: 0.281s
Producers: 8        Consumers: 1        Capacity: 2048      LinkedBlockingQueue: 0.555s     MpscUnboundedArrayQueue: 0.078s     MpscChunkedArrayQueue: 0.455s       Producers: 16       Consumers: 1        Capacity: 512       LinkedBlockingQueue: 2.676s     MpscUnboundedArrayQueue: 0.093s     MpscChunkedArrayQueue: 0.753s
Producers: 16       Consumers: 1        Capacity: 1024      LinkedBlockingQueue: 2.135s     MpscUnboundedArrayQueue: 0.093s     MpscChunkedArrayQueue: 0.792s
Producers: 16       Consumers: 1        Capacity: 2048      LinkedBlockingQueue: 0.944s     MpscUnboundedArrayQueue: 0.098s     MpscChunkedArrayQueue: 0.64s        Producers: 32       Consumers: 1        Capacity: 512       LinkedBlockingQueue: 6.647s     MpscUnboundedArrayQueue: 0.078s     MpscChunkedArrayQueue: 2.109s
Producers: 32       Consumers: 1        Capacity: 1024      LinkedBlockingQueue: 3.893s     MpscUnboundedArrayQueue: 0.095s     MpscChunkedArrayQueue: 1.797s
Producers: 32       Consumers: 1        Capacity: 2048      LinkedBlockingQueue: 2.019s     MpscUnboundedArrayQueue: 0.109s     MpscChunkedArrayQueue: 2.427s       Producers: 64       Consumers: 1        Capacity: 512       LinkedBlockingQueue: 26.59s     MpscUnboundedArrayQueue: 0.078s     MpscChunkedArrayQueue: 3.627s
Producers: 64       Consumers: 1        Capacity: 1024      LinkedBlockingQueue: 22.566s    MpscUnboundedArrayQueue: 0.093s     MpscChunkedArrayQueue: 3.047s
Producers: 64       Consumers: 1        Capacity: 2048      LinkedBlockingQueue: 1.719s     MpscUnboundedArrayQueue: 0.093s     MpscChunkedArrayQueue: 2.549s   3、结果分析(一):
通过结果打印耗时可以明显看到MpscUnboundedArrayQueue耗时几乎大多数都是不超过0.1s的,这添加、删除的操作效率不是一般的高,这也难怪人家netty要舍弃自己写的队列框架了;4、结果分析(二):
CompareQueueCosts代码里面我将ArrayList、LinkedList注释掉了,那是因为队列数量太大,List的操作太慢,效率低下,所以在大量并发的场景下,大家还是能避免则尽量避免,否则就遭殃了;

五、总结

1、通过底层无锁的Unsafe操作方式实现了多生产者同时访问队列的线程安全模型;2、由于使用锁会造成的线程切换,特别消耗资源,因此使用无锁而是采用CAS的操作方式,虽然会在一定程度上造成CPU使用率过高,但是整体上将效率还是听可观的;3、队列的数据结构是一种单向链表式的结构,通过生产、消费指针来标识添加、移除元素的指针位置,缓冲区与缓冲区之间通过指针指向,避免的数组的复制,较少了大量内存的占用情况;

六、下载地址

https://gitee.com/ylimhhmily/SpringCloudTutorial.git

SpringCloudTutorial交流QQ群: 235322432

SpringCloudTutorial交流微信群: 微信沟通群二维码图片链接

欢迎关注,您的肯定是对我最大的支持!!!

-
<上一篇        首页        下一篇>

原理剖析(第 012 篇)Netty之无锁队列MpscUnboundedArrayQueue原理分析相关推荐

  1. ZMQ无锁队列的原理与实现

    ZMQ无锁队列的原理与实现 前言 1. 为什么需要⽆锁队列 2. 无锁队列的实现(参考zmq,只支持一写一读的场景) 2.1 无锁队列前言 2.2 原⼦操作函数介绍 2.3 yqueue_t的chun ...

  2. 原理剖析-Netty之无锁队列

    一.大致介绍 1.了解过netty原理的童鞋,其实应该知道工作线程组的每个子线程都维护了一个任务队列: 2.细心的童鞋会发现netty的队列是重写了队列的实现方法,覆盖了父类中的LinkedBlock ...

  3. 无锁队列原理及实现(一)

    背景 在进行实际生产多线程开发的时候通常不会直接使用使用锁机制来操作线程间传递的数据,特别是对效率要求很高的场景中.最典型的就是音视频项目或者网络项目.这里先拿网络传输场景举例, 从这篇开始就开始详细 ...

  4. [数据结构]——无锁队列

    (152条消息) [数据结构]--无锁队列_lucky52529的博客-CSDN博客_无锁队列无锁队列 写这篇博客前想声名以下几点.第一,这篇文章重点内容是关于无锁队列如何实现,并不会深入讲解底层的C ...

  5. (Erlang语言)运行时中的无锁队列及其在异步线程中的应用

    本文首先介绍 Erlang 运行时中需要使用无锁队列的场合,然后介绍无锁队列的基本原理及会遇到的问题,接下来介绍 Erlang 运行时中如何通过"线程进度"机制解决无锁队列的问题, ...

  6. Erlang运行时中的无锁队列及其在异步线程中的应用

    本文首先介绍 Erlang 运行时中需要使用无锁队列的场合,然后介绍无锁队列的基本原理及会遇到的问题,接下来介绍 Erlang 运行时中如何通过"线程进度"机制解决无锁队列的问题, ...

  7. CAS无锁队列的实现

    文章目录 1. 基本原理 2. 代码实现 2.1 使用链表实现无锁队列 2.2 使用数组实现环形无锁队列 3. ABA 问题及解决 4. 参考资料 1. 基本原理 源于1994年10月发表在国际并行与 ...

  8. java 无锁队列实现_java无锁队列实现

    对于像应用中多个生产者需要并发发送一些日志信息给远程存储服务器,这些日志信息用于dubbo的调用链分析. 一种方案是生产者线程将要发送的日志消息存储到队列当中,然后由另一个本地消费线程从队列中获取要发 ...

  9. 地表最强队列-ZMQ无锁队列

    1 前言 老规矩,介绍前先简单聊一下为啥需要无锁队列,主要解决了哪些问题.首先是为啥需要无锁队列,我们最常见的就是利用锁保护临界资源,在多线程中进行队列操作,当并发量起来会带来大量的线程切换开销,而使 ...

最新文章

  1. leetcode算法题--连续的子数组的和
  2. Win10安装MySQL5.7.22 解压缩版(手动配置)方法
  3. 左侧固定,右侧自适应的布局方式(新增评论区大佬教的方法)
  4. 的run代码_小心使用 Task.Run 续篇
  5. 步步为营 .NET三层架构解析 七、UI的设计(登陆页面、注册页页和添加部门页面)...
  6. 多窗口售票:单件模式多线程实现
  7. 机器学习的基本概念和相关术语
  8. matlab虚拟现实之V-Realm Builder2使用NavigationInfo精确定位、建模
  9. /var/lock/subsys作用
  10. js navigator platform
  11. JDK的代码:抱怨FreeType的斜体不好用,自行处理
  12. php往pdf模板添加数据,php实现往pdf中加数字签名操作示例【附源码下载】
  13. C语言中TC20是什么意思,c语言tc20下载
  14. 视频分辨率过高,导致部分手机播放失败
  15. 全面了解信贷业务流程
  16. 英语翻译作业(十二)
  17. 急!灾区的食物依然短缺!(找不到原题出处只能这样了.....)
  18. 图形学:纹理寻址模式与UVTiling
  19. 独家全新娱乐性超高的喝酒神器微信小程序源码支持流量主解锁多人对战等等
  20. EMV技术学习和研究(三)应用初始化读应用数据

热门文章

  1. Fortran编程:(三)数据类型
  2. 超低功耗研发-STM32L151C8T6芯片(三)RTC自动唤醒机制
  3. 首期InnoSpace国际创业集训营举办DemoDay
  4. 14、守护线程(thread.setDaemon(true))
  5. 基于嘉立创双层板E5071C的低成本TRL校准
  6. Excel VBA - 操作文件
  7. docker容器创建的流程详解
  8. STM32的SRAM
  9. Java OpenCV-4.0.0 图像处理11 自定义图像滤波(降噪) 算子
  10. 操作系统学习(2) 进程管理