并发编程艺术笔记:并发队列、七大阻塞队列

目录

ConcurrentLinkedQueue

阻塞队列

1、ArrayBlockingQueue

2、LinkedBlockingQueue

3、PriorityBlockingQueue

4、DelayQueue

5、SynchronousQueue

6、LinkedTransferQueue

7、LinkedBlockingDeque


ConcurrentLinkedQueue

ConcurrentLinkedQueue是一个基于链接节点(单链表)的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法(即CAS算法)来实现。

ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和 指向下一个节点(next)的引用组成,节点与节点之间就是通过这个next关联起来,从而组成一个链表结构的队列。默认情况下head节点存储的元素为空,tail节点等于head节点。

入队

并发编程艺术笔记:并发队列、七大阻塞队列_第1张图片

并发编程艺术笔记:并发队列、七大阻塞队列_第2张图片

源码分析参考 并发容器之ConcurrentLinkedQueue

出队

并发编程艺术笔记:并发队列、七大阻塞队列_第3张图片

并发编程艺术笔记:并发队列、七大阻塞队列_第4张图片

通过updateHead方法更新head指向的节点以及构造哨兵节点(通过updateHead方法的h.lazySetNext(h)):

当head指向的节点的item域为null时会寻找真正的队头节点,等到待插入的节点插入之后,会更新head,并且将原来head指向的节点设置为哨兵节点。

源码分析参考 并发容器之ConcurrentLinkedQueue

HOPS设计

tail和head是延迟更新的,两者更新触发时机为:

tail更新触发时机:当tail指向的节点的下一个节点不为null的时候,会执行定位队列真正的队尾节点的操作,找到队尾节点后完成插入之后才会通过casTail进行tail更新;当tail指向的节点的下一个节点为null的时候,只插入节点不更新tail。

head更新触发时机:当head指向的节点的item域为null的时候,会执行定位队列真正的队头节点的操作,找到队头节点后完成删除之后才会通过updateHead进行head更新;当head指向的节点的item域不为null的时候,只删除节点不更新head。

以下摘自《Java并发编程的艺术》:

让tail节点永远作为队列的尾节点,这样实现代码量非常少,而且逻辑清晰和易懂。但是, 这么做有个缺点,每次都需要使用循环CAS更新tail节点。如果能减少CAS更新tail节点的次数,就能提高入队的效率,所以doug lea使用hops变量来控制并减少tail节点的更新频率,并不是每次节点入队后都将tail节点更新成尾节点,而是当tail节点和尾节点的距离大于等于常量 HOPS的值(默认等于1)时才更新tail节点,tail和尾节点的距离越长,使用CAS更新tail节点的次数就会越少,但是距离越长带来的负面效果就是每次入队时定位尾节点的时间就越长,因为循环体需要多循环一次来定位出尾节点,但是这样仍然能提高入队的效率。

因为从本质上来看它通过增加对volatile变量的读操作来减少对volatile变量的写操作,而对volatile变量的写操作开销要远远大于读操作,所以入队效率会有所提升。

出队也是通过hops变量来减少使用CAS更新head节点的消耗,从而提高出队效率。


阻塞队列

阻塞队列最核心的功能是,可阻塞式的插入和删除队列元素。当前队列为空时,会阻塞消费数据的线程,直至队列非空时,通知被阻塞的线程;当队列满时,会阻塞插入数据的线程,直至队列未满时,通知插入数据的线程。

1、ArrayBlockingQueue

数组实现的有界阻塞队列,按照FIFO原则

主要属性:

并发编程艺术笔记:并发队列、七大阻塞队列_第5张图片

构造函数:

并发编程艺术笔记:并发队列、七大阻塞队列_第6张图片

put方法:

并发编程艺术笔记:并发队列、七大阻塞队列_第7张图片

并发编程艺术笔记:并发队列、七大阻塞队列_第8张图片

take方法:

并发编程艺术笔记:并发队列、七大阻塞队列_第9张图片

并发编程艺术笔记:并发队列、七大阻塞队列_第10张图片

2、LinkedBlockingQueue

单向链表实现的有界阻塞队列,默认和最大长度为Integer.Max_VALUE,按照FIFO原则

主要属性:

并发编程艺术笔记:并发队列、七大阻塞队列_第11张图片

构造函数:

并发编程艺术笔记:并发队列、七大阻塞队列_第12张图片

put方法:

并发编程艺术笔记:并发队列、七大阻塞队列_第13张图片

take方法:

并发编程艺术笔记:并发队列、七大阻塞队列_第14张图片

3、PriorityBlockingQueue

优先级的无界阻塞队列,默认升序排列。不能保证同优先级元素的顺序。

主要属性:

并发编程艺术笔记:并发队列、七大阻塞队列_第15张图片

并发编程艺术笔记:并发队列、七大阻塞队列_第16张图片

构造函数:

并发编程艺术笔记:并发队列、七大阻塞队列_第17张图片

入队方法:

并发编程艺术笔记:并发队列、七大阻塞队列_第18张图片

由于队列是无限制的,因此offer方法永远不会返回false

入队时堆自下而上

并发编程艺术笔记:并发队列、七大阻塞队列_第19张图片

扩容方法:

扩容时使用一个单独变量的CAS操作来控制只有一个线程进行扩容

并发编程艺术笔记:并发队列、七大阻塞队列_第20张图片

出队方法:

并发编程艺术笔记:并发队列、七大阻塞队列_第21张图片

出队时堆自上而下

并发编程艺术笔记:并发队列、七大阻塞队列_第22张图片

4、DelayQueue

DelayQueue是无界阻塞队列,队列中的元素必须实现Delayed接口,元素过期后才会从队列中取走。

即如果一个类实现了Delayed接口,当创建该类的对象并添加到DelayQueue中后,只有当该对象的getDalay方法返回的剩余时间≤0时才会出队。由于DelayQueue内部委托了PriorityQueue对象来实现所有方法,所以能以堆的结构维护元素顺序,这样剩余时间最小的元素就在堆顶,每次出队其实就是删除剩余时间≤0的最小元素

主要属性:

并发编程艺术笔记:并发队列、七大阻塞队列_第23张图片

以下摘自:https://segmentfault.com/a/1190000016388106

DelayQueue每次只会出队一个过期的元素,如果队首元素没有过期,就会阻塞出队线程,让线程在available这个条件队列上无限等待。

为了提升性能,DelayQueue并不会让所有出队线程都无限等待,而是用leader保存了第一个尝试出队的线程,该线程的等待时间是队首元素的剩余有效期。这样,一旦leader线程被唤醒(此时队首元素也失效了),就可以出队成功,然后唤醒一个其它在available条件队列上等待的线程。之后,会重复上一步,新唤醒的线程可能取代成为新的leader线程。这样,就避免了无效的等待,提升了性能。这其实是一种名为“Leader-Follower pattern”的多线程设计模式。

构造方法:

并发编程艺术笔记:并发队列、七大阻塞队列_第24张图片

入队:

DelayQueue是阻塞队列,且优先级队列是无界的,所以入队不会阻塞不会超时,因此它的四个入队方法是一样的。

并发编程艺术笔记:并发队列、七大阻塞队列_第25张图片

出队:

并发编程艺术笔记:并发队列、七大阻塞队列_第26张图片

5、SynchronousQueue

不存储元素的阻塞队列,每一个put操作必须等待一个take操作,否则不能继续添加元素。

SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合传递性场景。SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。

主要属性:

并发编程艺术笔记:并发队列、七大阻塞队列_第27张图片

// Transferer抽象类,主要定义了一个transfer方法用来传输元素
abstract static class Transferer {
    abstract E transfer(E e, boolean timed, long nanos);
}

// 以栈方式实现的Transferer
static final class TransferStack extends Transferer {
    // 栈中节点的几种类型:
    static final int REQUEST    = 0;  //  消费者(请求数据的)
    static final int DATA       = 1;  //  生产者(提供数据的)
    static final int FULFILLING = 2;  //  二者正在匹配中

    // 栈中的节点
    static final class SNode {
        volatile SNode next;         // 下一个节点
        volatile SNode match;        // 匹配者
        volatile Thread waiter;      // 等待着的线程
        Object item;                 // 元素     
        int mode;          // 模式,也就是节点的类型,是消费者,是生产者,还是正在匹配中
    }
    // 栈头节点
    volatile SNode head;
}
// 以队列方式实现的Transferer
static final class TransferQueue extends Transferer {
    // 队列中的节点
    static final class QNode {
        volatile QNode next;        // 下一个节点
        volatile Object item;       // 存储的元素
        volatile Thread waiter;     // 等待着的线程
        final boolean isData;       // 是否是数据节点
    }
    // 队列的头节点
    transient volatile QNode head;
    // 队列的尾节点
    transient volatile QNode tail;
}

构造方法:

并发编程艺术笔记:并发队列、七大阻塞队列_第28张图片

无缓冲队列:

入队:

并发编程艺术笔记:并发队列、七大阻塞队列_第29张图片

出队:

并发编程艺术笔记:并发队列、七大阻塞队列_第30张图片

TransferStack.transfer()方法(自旋+CAS)

分为三种情况:

1、如果栈中没有元素,或者栈顶元素跟将要入栈的元素模式一样,就入栈

  • 入栈后自旋等待一会看有没有其它线程匹配到它,自旋完了还没匹配到元素就阻塞等待
  • 如果节点取消等待就调用clean方法清除取消等待的节点,并返回 null
  • 否则阻塞等待被唤醒了说明其它线程匹配到了当前的元素,就返回匹配到的元素

2、如果两者模式不一样,且头节点没有在匹配中,就拿当前节点跟它匹配,匹配成功了就返回匹配到的元素

3、如果两者模式不一样,且头节点正在匹配中,当前线程就协助去匹配,然后继续从第一步开始循环

TransferQueue.transfer()方法(自旋+CAS)

分为两种情况:

1、如果队列为空或者队列中的尾节点和自己模式相同,则把当前节点添加到队尾,并等待节点被匹配。匹配成功则返回匹配到的元素,如果等待节点被中断或等待超时返回null。在此期间会不断检查tail节点,如果tail节点被其他线程修改,则向后推进tail继续循环尝试

2、如果当前操作模式与尾节点tail不同,说明可以进行匹配,则从队列头节点head开始向后查找一个互补节点进行匹配,尝试通过CAS修改互补节点的item字段为给定元素e,匹配成功后向后推进head,并唤醒被匹配节点的waiter线程,最后返回匹配节点的item

源码分析参考 https://www.jianshu.com/p/c4855acb57ec

6、LinkedTransferQueue

无界阻塞队列,底层基于单链表实现。结点有两种类型:数据结点、请求结点。基于无锁算法实现。

transfer方法

用于将指定元素e传递给消费者线程。如果有消费者线程正在阻塞等待,则调用transfer方法的线程会直接将元素传递给它;如果没有消费者线程等待获取元素,则调用transfer方法的线程会将元素插入到队尾,然后阻塞等待,直到出现一个消费者线程获取元素。

自旋 -> yield -> 阻塞,线程不会立即进入阻塞,因为线程上下文切换的开销往往比较大,所以会先自旋一定次数,中途可能伴随随机的yield操作,让出cpu时间片,如果自旋次数用完后,还是没有匹配线程出现,再真正阻塞线程。(因为自旋会消耗CPU,因此自旋一定的次数后使用Thread.yield()方法来暂停当前正在执行的线程,并执行其他线程。)

tryTransfer方法

用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回。

源码分析参考Java多线程进阶(三八)—— J.U.C之collections框架:LinkedTransferQueue

7、LinkedBlockingDeque

近似有界阻塞队列,双端队列的结构,底层用双向链表实现,容量大小默认为Integer.MAX_VALUE。

主要属性:

并发编程艺术笔记:并发队列、七大阻塞队列_第31张图片

构造方法:

并发编程艺术笔记:并发队列、七大阻塞队列_第32张图片

并发编程艺术笔记:并发队列、七大阻塞队列_第33张图片

队首入队:

并发编程艺术笔记:并发队列、七大阻塞队列_第34张图片

并发编程艺术笔记:并发队列、七大阻塞队列_第35张图片

队首出队:

并发编程艺术笔记:并发队列、七大阻塞队列_第36张图片

并发编程艺术笔记:并发队列、七大阻塞队列_第37张图片

队尾入队:

并发编程艺术笔记:并发队列、七大阻塞队列_第38张图片

并发编程艺术笔记:并发队列、七大阻塞队列_第39张图片

队尾出队:

并发编程艺术笔记:并发队列、七大阻塞队列_第40张图片

并发编程艺术笔记:并发队列、七大阻塞队列_第41张图片

 

你可能感兴趣的:(并发编程)