java 标准输入 非阻塞 java非阻塞队列_阻塞队列


BlockingQueue家族(常用系列)

 BlockingQueue,顾名思义即是阻塞队列,意指再读取和插入操作情况下可能(注意是可能)会出现阻塞。

在并发编程中,有时候需要使用线程安全的队列。如果要实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法。

1.使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。

2.非阻塞的实现方式则可以使用循环CAS的方式来实现。

1.1 BlockingQueue

Java中的阻塞队列

什么是阻塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法, 意指再读取和插入操作情况下可能(注意是可能)会出现阻塞。

BlockingQueue本身是一个接口,主要定义了关于队列的各类操作方法,当发生无法继续入队或者无数据可以读出的时候,会发生如下图所示的情况。其中

Special Value为true,false,null

Blocks的方法会发生阻塞,

Throws Exception列内的方法会抛出异常,

Times Out指超过设定时间则会按照Special Value类型的方法返回true或者false。

java 标准输入 非阻塞 java非阻塞队列_python_02

1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。

1.2阻塞队列和生产者-消费者模式

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。

阻塞队列(Blocking queue)提供了可阻塞的put和take方法,它们与可定时的offer和poll是等价的。如果Queue已经满了,put方法会被阻塞直到有空间可用;如果Queue是空的,那么take方法会被阻塞,直到有元素可用。Queue的长度可以有限,也可以无限;无限的Queue永远不会充满,所以它的put方法永远不会阻塞。

阻塞队列简化了消费者的编码,因为take会保持阻塞直到可用数据出现。如果生产者不能足够快地产生工作,让消费者忙碌起来,那么消费者只能一直等待,直到有工作可做。同时,put方法的阻塞特性也大大地简化了生产者的编码;如果使用一个有界队列,那么当队列充满的时候,生产者就会阻塞,暂不能生成更多的工作,从而给消费者时间来赶进进度。

有界队列是强大的资源管理工具,用来建立可靠的应用程序:它们遏制那些可以产生过多工作量、具有威胁的活动,从而让你的程序在面对超负荷工作时更加健壮。

虽然生产者-消费者模式可以把生产者和消费者的代码相互解耦合,但是它们的行为还是间接地通过共享队列耦合在一起了

BlockingQueue本身只是一个接口,具体的实现交由其实现类进行定义设计,下面介绍几个实现类:

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • SynchronousQueue
  • PriorityBlockingQueue
  • DelayQueue

1.3ArrayBlockingQueue

ArrayBlockingQueue,相信大家看名字就能猜到,该阻塞队列是基于数组实现的,必须制定大小且不可变,同时使用ReentrantLock来实现并发问题的解决。同时需要注意的是ArrayBlockingQueue只有一把锁,put和take操作会相互阻塞。我们看一下其构造函数即可清楚知道

java 标准输入 非阻塞 java非阻塞队列_java_03

java 标准输入 非阻塞 java非阻塞队列_python_04

1.4LinkedBlockingQueue

LinkedBlockingQueue和ArrayBlockingQueue十分相似,其底层是借由链表实现。除此之外,还有一个不同点,LinkedBlockingQueue拥有两个锁,因此put和take的线程可以同时运行

java 标准输入 非阻塞 java非阻塞队列_数据结构与算法_05

阻塞队列LinkedBlockingQueue是一个单向链表实现的阻塞队列,先进先出的顺序并且是无界队列,默认使用int的最大值。继承AbstractQueue,实现了BlockingQueue,Serializable接口。插入和取出使用不同的锁,putLock插入锁,takeLock取出锁,注意不接受null,添加和删除数据的时候可以并行。主要作为固定大小线程池(Executors.newFixedThreadPool())底层所使用的阻塞队列使用。

java 标准输入 非阻塞 java非阻塞队列_java 标准输入 非阻塞_06

写:

add方法:如果队列已满,则抛出异常;

put方法:向队列尾部添加元素,队列已满的时候,阻塞等待。对应读方法take

offer方法:向队列尾部添加元素,队列已满的时候,直接返回false。

读:

remove移除并返回队列头部的元素,如果队列已空,则抛出NoSuchElementException异常.

element返回队列头部的元素,如果队列为空.则抛出NoSuchElementException异常.

poll移除并返回队列头部的元素,如果队列为空,则返回null;

peek返回队列头部的元素,如果队列为空,则返回null;

take移除并返回队列头部的元素,如果队列为空则阻塞;

读写操作时用到的锁:

当执行take、poll等操作时线程需要获取的锁

ReentrantLock takeLock = new ReentrantLock();

当执行add、put、offer等操作时线程需要获取锁

ReentrantLock putLock = new ReentrantLock();

1.5SynchronousQueue

SynchronousQueue的特点是只能容纳一个元素,同时SynchronousQueue使用了两种模式来管理元素,一种是使用先进先出的队列,一种是使用后进先出的栈,使用哪种模式可以通过构造函数来指定。

java 标准输入 非阻塞 java非阻塞队列_java_07

1.6、PriorityBlockingQueue

PriorityBlockingQueue顾名思义,按照优先级排序且无边界的队列。插入其中的元素必须实现java.lang.Comparable接口。其排序规则就按此实现。

1.7DelayQueue

DelayQueue即为延迟队列,使得插入其中的元素在延迟一定时间后,才能获取到,插入其中的元素需要实现java.util.concurrent.Delayed接口。该接口需要实现getDelay()和compareTo()方法。getDealy()返回0或者小于0的值时,delayedQueue通过其take()方法就可以获得此元素。compareTo()方法用于实现内部元素的排序,一般情况,按元素过期时间的优先级进行排序是比较好的选择。下面我们通过一个示例来演示一下DelayQueue的使用

java 标准输入 非阻塞 java非阻塞队列_阻塞队列_08

java 标准输入 非阻塞 java非阻塞队列_java_09

JDK 7提供了7个阻塞队列,如下。
·ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
·LinkedBlockingQueue:一个由链表结构组成的有界int最大值阻塞队列。
·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
·DelayQueue:一个使用优先级队列实现的无界阻塞队列。
·SynchronousQueue:一个不存储元素的阻塞队列。
·LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

2.1.ConcurrentLinkedQueue(非阻塞)

 

我们一起来研究一下如何使用非阻塞的方式来实现线程安全队列ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。

public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
        implements Queue<E>, java.io.Serializable {
    private transient volatile Node<E> head;//头指针
    private transient volatile Node<E> tail;//尾指针
    public ConcurrentLinkedQueue() {//初始化,head=tail=(一个空的头结点)
        head = tail = new Node<E>(null);
    }
    private static class Node<E> {
        volatile E item;
        volatile Node<E> next;//内部是使用单向链表实现
        ......
    }
    ......

}

java 标准输入 非阻塞 java非阻塞队列_数据结构与算法_10

public boolean offer(E e) {
        checkNotNull(e);
final Node<E> newNode = new Node<E>(e);//入队前,创建一个新节点
   for (Node<E> t = tail, p = t;;) {//除非插入成功并返回,否则反复循环
            Node<E> q = p.next;
            if (q == null) {
                // p is last node
                if (p.casNext(null, newNode)) {//利用CAS操作,将p的next指针从旧值null更新为newNode
                    if (p != t) // hop two nodes at a time
                        casTail(t, newNode);  // Failure is OK.利用CAS操作更新tail,如果失败说明其他线程添加了元素,由其他线程负责更新tail
                    return true;
                }
                // Lost CAS race to another thread; re-read next 如果添加元素失败,说明其他线程添加了元素,p后移,并继续尝试
            }
            else if (p == q) //如果p被移除出链表,我们需要调整指针重新指向head,否则我们指向新的tail
                p = (t != (t = tail)) ? t : head;
            else
                //p指向tail或者q
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

casTail(cmp,value)方法用于更新tail节点。tail被设置为volatile保证可见性。

p.casNext(cmp,value)方法用于将入队节点设置为当前队列尾节点的next节点。value也被设置为volatile。

对于出队操作,也是使用CAS的方式循环尝试将元素从头部移除。

因为采用CAS操作,允许多个线程并发执行,并且不会因为加锁而阻塞线程,使得并发性能更好。

关键解释:

原子性与可见性分析之synchronized;volatile

原子性

原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。

可见性

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。

volatile 本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,

synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.