阻塞队列 BlockingQueue(一):7个阻塞队列简介

线程安全的队列访问

主要应用场景:生产者消费者模型,是线程安全的
BlockingQueue线程安全
阻塞情况:

  • 当队列满了进行入队操作
  • 当队列空了的时候进行出队列操作

4种处理方式

插入和移除操作的4中处理方式

  • (Throws Exceptions) 抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
  • (Special Value)返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。
  • (Blocks)一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
  • (Times Out)超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。

注意: 如果是无界阻塞队列,队列不可能会出现满的情况,所以使用put或offer方法永远不会被阻塞,而且使用offer方法时,该方法永远返回true。

实现类:

Java并发包中的7个阻塞队列:

队列有界性特点
ArrayBlockingQueuebounded(有界)加锁数组 FIFO
LinkedBlockingQueue指定-有界、不指定-无界加锁链表 FIFO
PriorityBlockingQueue无界加锁优先级排序(默认升序)
DelayQueue无界加锁延时获取 使用PriorityQueue实现
SynchronousQueuebounded加锁不存储元素
LinkedTransferQueue无界加锁链表 (传输)
LinkedBlockingDeque无界无锁链表 双向

默认情况下线程采用非公平性策略访问队列

SynchronusQueue

不存储元素(没有任何容量),非常适合传递性场景,吞吐量较高。每一个put操作必须等待一个take操作,否则不能继续添加元素。

它支持公平访问队列。

//创建公平性访问 fair=true,则等待的线程会采用先进先出的顺序访问队列。
public SynchronousQueue(boolean fair) {
	transferer = fair?new TransferQueue() : new TransferStack();
}

ArrayBlockingQueue

初始化时指定容量大小,有利于防止资源耗尽;一旦指定大小就不能再变。采用FIFO方式存储元素。

  • 默认情况下不保证线程公平的访问队列
  • 公平访问队列是指阻塞的线程,可以按照阻塞的先后顺序访问队列,即先阻塞线程先访问队列。
  • 非公平性是对先等待的线程是非公平的,当队列可用时,阻塞的线程都可以争夺访问队列的资格,有可能先阻塞的线程最后才访问 队列。
  • 为了保证公平性,通常会降低吞吐量。

使用以下代码创建一个公平的阻塞队列。

ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);

访问者的公平性是使用可重入锁实现的,代码如下。

public ArrayBlockingQueue(int capacity, boolean fair) { 
  if (capacity <= 0)  
    throw new IllegalArgumentException(); 
  this.items = new Object[capacity]; 
  lock = new ReentrantLock(fair);//初始化ReentrantLock重入锁,出队入队拥有这同一个锁 
  notEmpty = lock.newCondition;//初始化非空等待队列
  notFull = lock.newCondition;//初始化非满等待队列 
} 

LinkedBlockingQueue

大小配置可选,如果初始化时指定了大小,那么它就是有边界的。不指定就无边界(最大整型值)。内部实现是链表,采用FIFO形式保存数据。

使用无界队列后,当核心线程都繁忙时,后续任务可以无限加入队列,线程池中线程数不会超过核心线程数。可提高线程池吞吐量,但代价是牺牲内存空间,甚至会导致内存溢出。

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);//不指定大小,无边界采用默认值,最大整型值
}

PriorityBlockingQueue

带优先级的阻塞队列(默认升序)。无边界队列,允许插入null 。必须实现Comparable接口,这样才能通过实现compareTo()方法进行排序。优先级最高的元素将始终排在队列的头部。
不会保证优先级一样的元素的排序,也不保证当前队列中除了优先级最高的元素以外的元素,随时处于正确排序的位置。
我们可以从PriorityBlockingQueue中获取一个迭代器,但这个 迭代器并不保证能按照优先级的顺序进行迭代

public boolean add(E e) {//添加方法
    return offer(e);
}
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    int n, cap;
    Object[] array;
    while ((n = size) >= (cap = (array = queue).length))
        tryGrow(array, cap);
    try {
        Comparator<? super E> cmp = comparator;//必须实现Comparator接口
        if (cmp == null)
            siftUpComparable(n, e, array);
        else
            siftUpUsingComparator(n, e, array, cmp);
        size = n + 1;
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}

LinkedTransferQueue

由链表结构组成的无界阻塞队列,采用一种预占模式:消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素。

相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。

(1)transfer方法

  • 如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。
  • 如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回
//试图把存放当前元素的s节点作为tail节点
Node pred = tryAppend(s, haveData);
//CPU自旋等待消费者消费元素。
//因为自旋会消耗CPU,所以自旋一定的次数后使用Thread.yield()方法来暂停当前正在执行的线程,并执行其他线程。
return awaitMatch(s, pred, e, (how == TIMED), nanos);

(2)tryTransfer方法

  • tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。
  • 对于带有时间限制的tryTransfer(E e,long timeout,TimeUnit unit)方法,试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在时限内消费了元素,则返回true。

(3)区别
tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回。

LinkedBlockingDeque

LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列(可以从队列的两端插入和移出元素,减少了一半的竞争)。
相比其他的阻塞队列,LinkedBlockingDeque多了
addFirst、offerFirst、peekFirst
addLast、offerLast、peekLast 等方法
以First单词结尾的方法,表示插入、获取(peek)或移除双端队列的第一个元素。
以Last单词结尾的方法,表示插入、获取(peek)或移除双端队列的最后一个元素。

插入方法add等同于addLast
移除方法remove等效于removeFirst,但是take方法却等同于takeFirst。
使用时还是用带有First和Last后缀的方法更清楚。

在初始化LinkedBlockingDeque时可以设置容量防止其过度膨胀。另外,双向阻塞队列可以运用在“工作窃取”模式中。

阻塞队列的实现原理

使用通知模式实现:当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。(JDK源码ArrayBlockingQueue使用了Condition来实现)

当往队列里插入一个元素时,如果队列不可用,那么阻塞生产者主要通过LockSupport.park(this)来实现。

参考:
慕课网实战·高并发探索(十三):并发容器J.U.C – 组件FutureTask、ForkJoin、BlockingQueue
阻塞队列 BlockingQueue
BlockingQueue深入解析
《java并发编程的艺术》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值