Java concurrent programming - BlockingQueue

Introduction

BlockingQueue is a good solution to the problem of how to efficiently and safely "transmit" data in multithreading. Through these efficient and thread-safe queue classes, it brings great convenience for us to quickly build high-quality multi-threaded programs.

 

        A blocking queue is a queue, and it is a first-in, first-out queue (FIFO).

       In a multi-threaded environment, data sharing can be easily achieved through queues. For example, in the classic "producer" and "consumer" models, data sharing between the two can be easily achieved through queues. Suppose we have several producer threads and several other consumer threads. If the producer thread needs to share the prepared data with the consumer thread, and use the queue to transmit the data, the data sharing problem between them can be easily solved .

          But what if the producer and the consumer are in a certain period of time, in case the data processing speed does not match? Ideally, if the rate at which the producer produces data is greater than the rate at which the consumer consumes, and when the produced data accumulates to a certain level, the producer must pause and wait (block the producer thread) in order to wait for the consumer. The thread finishes processing the accumulated data, and vice versa. However, before the concurrent package was released, in a multi-threaded environment, each of us programmers had to control these details, especially with regard to efficiency and thread safety, which would bring a lot of complexity to our programs . Fortunately, at this time, a powerful concurrent package was born, and he also brought us a powerful BlockingQueue. (In the field of multi-threading: the so-called blocking, in some cases, the thread will be suspended (ie blocked), and once the condition is met, the suspended thread will be automatically awakened)

As shown in the figure above: when the queue is full of data, all threads on the producer side will be automatically blocked (suspended) until there is an empty position in the queue, and the threads will be automatically awakened.
     This is why we need BlockingQueue in a multithreaded environment. As a user of BlockingQueue, we no longer need to care about when we need to block the thread and when we need to wake up the thread, because BlockingQueue does it all for you.

Introduction to the BlockingQueue interface

Put data:
  offer(anObject): means that if possible, add anObject to the BlockingQueue, that is, if the BlockingQueue can accommodate,
    return true, otherwise return false. (This method does not block the thread currently executing the method)
  offer(E o , long timeout, TimeUnit unit), you can set the waiting time, if the
    BlockingQueue cannot be added to the queue within the specified time , it will return a failure.
  put(anObject): Add anObject to the BlockingQueue. If there is no space in the BlockingQueue, the thread calling this method will be blocked
    until there is space in the BlockingQueue to continue.
Get data:
  poll(time): Take the object ranked first in the BlockingQueue. If you can't take it out immediately, you can wait for the time specified by the time parameter. If you can't take
    it, return null;
  poll(long timeout, TimeUnit unit): From the BlockingQueue Take out an object at the head of the queue, and if there is data available in the queue within the specified time,
    the data in the queue will be returned immediately. Otherwise, if the time expires and there is no data available, return failure.
take(): Take the first object in the BlockingQueue. If the BlockingQueue is empty, block and enter the waiting state until
new data is added to the BlockingQueue;
  drainTo(): ​​Get all the available data objects from the BlockingQueue at one time (you can also Specify the number of data to obtain), 
    Through this method, the efficiency of data acquisition can be improved; there is no need to lock or release locks in batches multiple times.

这里写图片æè¿°

Apply practice

  Producer

 

  Customer

 

  同时生产与消费

上述代码的示意图如下,开辟了三个线作为生产者,一个线程作为消费者。生产者负责往队列中添加数据,消费者负责从队列中消费数据(当队列中没有数据时则处于阻塞状态)

-23_thumb

 执行结果

        从上面的运用实践中很容易理解阻塞队列的好处,让设计的隔离度更好,生产者只负责生产数据消费者只负责消费数据,而不用关心队列中具体有多少数据,如果满和空的特殊处理也不用关心。

      可以想象一下如果没有阻塞队列,自己定义一个数组存放元素,生产者和消费者需要做很多额外的控制工作,并对边界条件做特殊处理。最重要的一点是生产者和消费者还要保证多线程操作数组数据的安全性同时兼顾效率,这应该是件很头疼的事。

      这里可能有个疑惑, 3个Producer产生数据,当队列已经满时,其它Producer如何再往队列里面生产数据?

      可以看到Producer中的代码,通过 offer(data, 2, TimeUnit.SECONDS) 往队列中添加数据,此时如果队列已满则阻塞等待直到Customer从队列中取走一个数据,然后再将数据放入,这里等待的时间不等。队列满时,offer()函数从开始执行到结束可能需要经历0~2000ms。从执行结果看,所有数据都成功的加入了队列没有出现超时的现象。

 

ArrayBlockingQueue源码分析

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable

可以看出ArrayBlockingQueue不光实现了BlockingQueue接口还继承了抽象类AbstractQueue,说明可以对进行队列的操作(可以参考java容器类4:Queue深入解读)。建议先了解可重入锁和条件变量的概念:

java并发编程——通过ReentrantLock,Condition实现银行存取款

下面看一下里面的主要成员变量

复制代码
 /** The queued items */
    final Object[] items;

    /** items index for next take, poll, peek or remove */
    int takeIndex;

    /** items index for next put, offer, or add */
    int putIndex;

    /** Number of elements in the queue */
    int count;

    /*
     * Concurrency control uses the classic two-condition algorithm
     * found in any textbook.
     */

    /** Main lock guarding all access */
    // 主要解决多线程访问的线程安全性问题
    final ReentrantLock lock;

    /** Condition for waiting takes */
// 添加元素时,通过notEmpty 唤醒消费线程(在等待该条件)
    private final Condition notEmpty;

    /** Condition for waiting puts */
  // 删除元素时,通过 notFull 唤醒生成线程(在等待该条件)
    private final Condition notFull;
复制代码

通过一个数组存放队列元素,并且通过维护一个插入元素(putIndex)和移除元素(takeIndex)的位置来控制元素的添加和删除。

看一下里面比较复杂的函数,大概能了解ArrayBlockingQueue的具体工作原理了:

复制代码
 public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        Objects.requireNonNull(e);
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length) {
                if (nanos <= 0L)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }
复制代码

该函数是往阻塞队列中添加元素,如果超过设置的时间还没有添加成功(可能队列已满,且没有其它线程从中移除元素)则返回false。源码中可以看出,当执行添加时,首先获取阻塞队列的锁,如果队列未满则直接添加元素返回true即可。

当队列已满,则调用 notFull(Condition类型)的awaitNanos()方法,该方法或释放可重入锁,并且让线程进入等待状态,知道有其它线程将该线程唤醒。enqueue的源码中会调用 notEmpty.signal()方法唤醒阻塞的移除元素的线程。同理,当某个线程调用take()/remove()/poll()时会调用 notFull.signal()唤醒一个被阻塞的添加元素的线程。

 

LinkedBlockingQueue

构造函数

public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}

成员变量

复制代码
 private final int capacity;

    /** Current number of elements */
    private final AtomicInteger count = new AtomicInteger();

    /**
     * Head of linked list.
     * Invariant: head.item == null
     */
    transient Node<E> head;

    /**
     * Tail of linked list.
     * Invariant: last.next == null
     */
    private transient Node<E> last;

    /** Lock held by take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();

    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();
复制代码

可以发现LinkedBlockingQueue和ArrayBlockingQueue成员变量还是有差别的

1.它内部是通过链表存储的,而ArrayBlockingQueue是通过数组存储的

2. 它设置了两个可重入锁,一个控制存,一个控制取。 (感觉这样并发性更好)

3. 它的计数器count采用: AtomicInteger ,而ArrayBlockingQueue采用的int。 可能原因: 在LinkedBlockingQueue中两端都可以同时进行存取操作(因为不是同一个锁,这时可能需要同时改变计数器的值,所以要保证线程安全,所有用了AtomicInteger ),而在ArrayBlockingQueue中不可能存在多个线程操作count值的情况,所以直接使用了int。

-25_thumb

上图中画出了LinkedBlockingQueue的工作机制,通过takeLock,putLock两把锁分别控制取数据和存数据,两者可以同时进。 下面可以看一下取数据的源码,其实很简单:

复制代码
 public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
// 获取取数据的锁
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
//如果没有数据,则进入挂起状态,直到“存操作”唤醒该挂起装填
            while (count.get() == 0) {
                notEmpty.await();
            }
  
// 将数据弹出队列,并将计数器减一
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)    
// 如果有挂起的存线程,则将其唤醒
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }
复制代码

DelayQueue

DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
使用场景:
  DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。

PriorityBlockingQueue

         基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。

SynchronousQueue

一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。
  声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:
  如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
  但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。

小结

         BlockingQueue不光实现了一个完整队列所具有的基本功能,同时在多线程环境下,他还自动管理了多线间的自动等待与唤醒功能,从而使得程序员可以忽略这些细节,关注更高级的功能。

Java 并发编程系列文章

Java 并发基础——线程安全性

java 并发编程——Thread 源码重新学习

Java 并发编程——Callable+Future+FutureTask

Java 并发编程——Executor框架和线程池原理

java并发编程——通过ReentrantLock,Condition实现银行存取款

http://news.kpx1618.cn/
http://news.prl0026.cn/
http://news.bxb7451.cn/
http://news.ube1531.cn/
http://news.qnu9925.cn/
http://news.hxl6493.cn/
http://news.ric5056.cn/
http://news.ibs2142.cn/
http://news.mco2769.cn/
http://news.tzr5175.cn/
http://news.vsb9575.cn/
http://news.eho1460.cn/
http://news.dyy3200.cn/
http://news.tdw5546.cn/
http://news.mtj9347.cn/
http://news.osi0013.cn/
http://news.bjb5476.cn/
http://news.ghk5310.cn/
http://news.xjy3902.cn/
http://news.grp2563.cn/
http://news.lkg4662.cn/
http://news.vwb8311.cn/
http://news.mmw6064.cn/
http://news.cqz7056.cn/
http://news.nlk4583.cn/
http://news.adw2245.cn/
http://news.alj9141.cn/
http://news.vdf1425.cn/
http://news.miv2453.cn/
http://news.vdx0926.cn/
http://news.smc5776.cn/
http://news.ffn3573.cn/
http://news.rdj9135.cn/
http://news.mtu9335.cn/
http://news.gzv8338.cn/
http://news.xum5501.cn/
http://news.jiq1934.cn/
http://news.syh5891.cn/
http://news.yvr8830.cn/
http://news.aua2439.cn/
http://news.ath0401.cn/
http://news.gmx2930.cn/
http://news.pzf7790.cn/
http://news.ass0795.cn/
http://news.mox2684.cn/
http://news.oqc1977.cn/
http://news.bcu6005.cn/
http://news.ajj5951.cn/
http://news.xwt5617.cn/
http://news.rlv0165.cn/
http://news.shg1037.cn/
http://news.akj0836.cn/
http://news.ipc6507.cn/
http://news.kri6555.cn/
http://news.mzj8672.cn/
http://news.azq7227.cn/

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326736275&siteId=291194637