深入剖析阻塞队列BlockingQueue (详解ArrayBlockingQueue和LinkedBlockingQueue及其应用)

前言

这篇博客南国主要讲解关于Java中阻塞队列的知识点,提到阻塞队列(BlockingQueue)想必大家最先想到的是生产者-消费者,诚然这也是阻塞队列最直接的应用场景。 本篇分为四个章节,BlockingQueue简介,常见的基本操作,常用的BlockingQueue实现类和应用demo。这里针对BlockingQueue的应用南国主要写了生产者-消费者的实现和线程通信的实现。前三个部分的基础知识总结,很多内容参考了并发容器之BlockingQueue的叙述,结合自己的理解南国 在一些内容上做了增加和重新编辑。
话不多说,干货送上~

1. BlockingQueue简介

在实际编程中,会经常使用到JDK中Collection集合框架中的各种容器类如实现List,Map,Queue接口的容器类,但是这些容器类基本上不是线程安全的,除了使用Collections可以将其转换为线程安全的容器,Doug Lea大师为我们都准备了对应的线程安全的容器,如实现List接口的CopyOnWriteArrayList,实现Map接口的ConcurrentHashMap,实现Queue接口的ConcurrentLinkedQueue

在我们学习操作系统时遇到的一个最经典的"生产者-消费者"问题中,队列通常被视作线程间操作的数据容器,这样,可以对各个模块的业务功能进行解耦,生产者将“生产”出来的数据放置在数据容器中,而消费者仅仅只需要在“数据容器”中进行获取数据即可,这样生产者线程和消费者线程就能够进行解耦,只专注于自己的业务功能即可。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是BlockingQueue提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止

2. 常见的基本操作

BlockingQueue基本操作总结如下:
在这里插入图片描述
BlockingQueue继承于Queue接口,因此,对数据元素的基本操作有:

插入元素:
add(E e) :往队列插入数据,当队列满时,插入元素时会抛出IllegalStateException异常;
offer(E e):当往队列插入数据时,插入成功返回true,否则则返回false。当队列满时不会抛出异常;

删除元素:
remove(Object o):从队列中删除数据,成功则返回true,否则为false
poll:删除数据,当队列为空时,返回null;

查看元素:
element:获取队头元素,如果队列为空时则抛出NoSuchElementException异常;
peek:获取队头元素,如果队列为空则抛出NoSuchElementException异常

接下来,南国讲一下BlockingQueue具有的特殊操作

插入数据:
put:当阻塞队列容量已经满时,往阻塞队列插入数据的线程会被阻塞,直至阻塞队列已经有空余的容量可供使用;
offer(E e, long timeout, TimeUnit unit):若阻塞队列已经满时,同样会阻塞插入数据的线程,直至阻塞队列已经有空余的地方,与put方法不同的是,该方法会有一个超时时间,若超过当前给定的超时时间,插入数据的线程会退出;

删除数据:
take():当阻塞队列为空时,获取队头数据的线程会被阻塞;
poll(long timeout, TimeUnit unit):当阻塞队列为空时,获取数据的线程会被阻塞,另外,如果被阻塞的线程超过了给定的时长,该线程会退出

3. 常用的BlockingQueue

实现BlockingQueue接口的有ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedTransferQueue, PriorityBlockingQueue, SynchronousQueue,而这几种常见的阻塞队列也是在实际编程中会常用的,下面对这几种常见的阻塞队列进行说明:

3.1. ArrayBlockingQueue

ArrayBlockingQueue是由数组实现的有界阻塞队列。该队列命令元素FIFO(先进先出)。因此,对头元素head是队列中存在时间最长的数据元素,而对尾数据tail则是当前队列最新的数据元素。ArrayBlockingQueue可作为“有界数据缓冲区”,生产者插入数据到队列容器中,并由消费者提取。ArrayBlockingQueue一旦创建,容量不能改变。

当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。

ArrayBlockingQueue默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到ArrayBlockingQueue。而非公平性则是指访问ArrayBlockingQueue的顺序不是遵守严格的时间顺序,有可能存在,一旦ArrayBlockingQueue可以被访问时,长时间阻塞的线程依然无法访问到ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的ArrayBlockingQueue,可采用如下代码:

private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);
3.1.1 ArrayBlockingQueue的主要属性

ArrayBlockingQueue的主要属性如下:

/** 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 */
private final Condition notEmpty;

/** Condition for waiting puts */
private final Condition notFull;

从源码中可以看出ArrayBlockingQueue内部是采用数组进行数据存储的(属性items),为了保证线程安全,采用的是ReentrantLock lock,为了保证可阻塞式的插入删除数据利用的是Condition,当获取数据的消费者线程发现阻塞队列为空时会调用notEmpty.await()挂起消费者 非空会去阻塞队列获取数据消费并且调用notFull.signal 告知生产者队列未满,同理 当插入数据的生产者线程发现队列已满时会调用notFull.await()挂起生产者 如果未满则往队列写入元素并调用notEmpty.signal() 通知消费者来消费。而notEmpty和notFull等中要属性在构造方法中进行创建:

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

接下来,主要看看可阻塞式的put和take方法是怎样实现的。

3.1.2 put方法详解

put(E e)方法源码如下:

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
		//如果当前队列已满,将线程移入到notFull等待队列中
        while (count == items.length)
            notFull.await();
		//满足插入数据的要求,直接进行入队操作
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

该方法的逻辑很简单,当队列已满时(count == items.length)调用notFull.await() 挂起线程;,如果当前满足插入数据的条件,就可以直接调用 enqueue(e)插入数据元素。enqueue方法源码为:

private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
	//插入数据
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
	//通知消费者线程,当前队列中有数据可供消费
    notEmpty.signal();
}

enqueue方法的逻辑同样也很简单,先完成插入数据,即往数组中添加数据(items[putIndex] = x),然后通知被阻塞的消费者线程,当前队列中有数据可供消费(notEmpty.signal())。

3.1.3 take方法详解

take方法源码如下:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
		//如果队列为空,没有数据,将消费者线程移入等待队列中
        while (count == 0)
            notEmpty.await();
		//获取数据
        return dequeue();
    } finally {
        lock.unlock();
    }
}

take方法也主要做了两步:1. 如果当前队列为空的话, notEmpty.await(); 挂起当前线程;2. 若队列不为空则获取数据,即完成出队操作dequeue。dequeue方法源码为:

private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
	//获取数据
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    //通知被阻塞的生产者线程
	notFull.signal();
    return x;
}

dequeue方法也主要做了两件事情:1. 获取队列中的数据,即获取数组中的数据元素((E) items[takeIndex]);2. 通知notFull等待队列中的线程,使其由等待队列移入到同步队列中,使其能够有机会获得lock,并执行完成功退出。

从以上分析,可以看出put和take方法主要是通过condition的通知机制来完成可阻塞式的插入数据和获取数据。在理解ArrayBlockingQueue后再去理解LinkedBlockingQueue就很容易了。

3.2. LinkedBlockingQueue

LinkedBlockingQueue是用链表实现的阻塞队列,同样满足FIFO的特性。与ArrayBlockingQueue相比起来具有更高的吞吐量,为了防止LinkedBlockingQueue容量迅速增,损耗大量内,通常在创建LinkedBlockingQueue对象时,会指定其大小(如果指定了大小,我们判定它为有界队列),如果未指定,容量等于Integer.MAX_VALUE(我们视它为无界队列)。查看它的构造方法:

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}
3.2.1 LinkedBlockingQueue的主要属性

LinkedBlockingQueue的主要属性有:

/** 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();

可以看出与ArrayBlockingQueue主要的区别是,LinkedBlockingQueue在插入数据和删除数据时分别是由两个不同的lock(takeLock和putLock)来控制线程安全的,因此,也由这两个lock生成了两个对应的condition(notEmpty和notFull)来实现可阻塞的插入和删除数据。并且,采用了链表的数据结构来实现队列,Node结点的定义为:

static class Node<E> {
    E item;

    /**
     * One of:
     * - the real successor Node
     * - this Node, meaning the successor is head.next
     * - null, meaning there is no successor (this is the last node)
     */
    Node<E> next;

    Node(E x) { item = x; }
}

接下来,我们也同样来看看put方法和take方法的实现。

3.2.2 put方法详解

put方法源码为:

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    // Note: convention in all put/take/etc is to preset local var
    // holding count negative to indicate failure unless set.
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        /*
         * Note that count is used in wait guard even though it is
         * not protected by lock. This works because count can
         * only decrease at this point (all other puts are shut
         * out by lock), and we (or some other waiting put) are
         * signalled if it ever changes from capacity. Similarly
         * for all other uses of count in other wait guards.
         */
		//如果队列已满,则阻塞当前线程,将其移入等待队列
        while (count.get() == capacity) {
            notFull.await();
        }
		//入队操作,插入数据
        enqueue(node);
        c = count.getAndIncrement();
		//若队列满足插入数据的条件,则通知被阻塞的生产者线程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
}

put方法的逻辑也同样很容易理解,可见注释。基本上和ArrayBlockingQueue的put方法一样。

3.2.3 take方法的源码如下:
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;
}

take方法的主要逻辑请见于注释,也很容易理解。

3.2.4. ArrayBlockingQueue与LinkedBlockingQueue的比较(重要)

相同点:ArrayBlockingQueue和LinkedBlockingQueue都是通过condition通知机制来实现可阻塞式插入和删除元素,并满足线程安全的特性;

不同点:1. ArrayBlockingQueue底层是采用的数组进行实现,而LinkedBlockingQueue则是采用链表数据结构; 2. ArrayBlockingQueue插入和删除数据,只采用了一个lock,而LinkedBlockingQueue则是在插入和删除分别采用了putLock和takeLock,这样可以降低线程由于线程无法获取到lock而进入WAITING状态的可能性,从而提高了线程并发执行的效率。

3.3. PriorityBlockingQueue

PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现compareTo()方法来指定元素排序规则,或者初始化时通过构造器参数Comparator来指定排序规则。

package Concurrent.Blockingqueue;

import lombok.Data;

import java.util.PriorityQueue;
import java.util.concurrent.ThreadLocalRandom;

/**
 * @author xiejiahao
 * @version 1.0
 * @description: PriorityBlockingQueue 简单使用
 * 与ArrayBlockingQueue LinkedBlockingQueue二者是有界阻塞队列不同的是,PriorityBlockingQueue是一个无界阻塞队列
 * 执行结果说明,任务执行的先后顺序和入堆的先后顺序无关 而是和优先级有关系
 * @date 2021/6/13 15:04
 */
public class PriorityQueue_30 {
    @Data
    static class Task implements Comparable<Task> {

        private int priority = 0;

        private String taskName;

        @Override
        public int compareTo(Task o) {
            if (this.priority >= o.priority) {
                return 1;
            } else return -1;
        }

        private void printTaskAndPriority() {
            System.out.println("taskName: " + taskName + ", priority: " + priority);
        }
    }

    public static void main(String[] args) {
        PriorityQueue<Task> priorityQueue = new PriorityQueue<>();
        for (int i = 0; i < 10; i++) {
            Task task = new Task();
            task.setPriority(ThreadLocalRandom.current().nextInt(10));
            task.setTaskName("taskName: " + i);
            priorityQueue.offer(task); //入堆
        }

        // 取出任务执行
        while (!priorityQueue.isEmpty()) {
            Task task = priorityQueue.poll(); //取出堆顶元素
            if (null != task) {
                task.printTaskAndPriority();
            }
        }
    }
}

3.4. SynchronousQueue

它的实质是一种无缓冲的等待队列。SynchronousQueue每个插入操作必须等待另一个线程进行相应的删除操作,因此,SynchronousQueue实际上没有存储任何数据元素,因为只有线程在删除数据时,其他线程才能插入数据,同样的,如果当前有线程在插入数据时,线程才能删除数据。SynchronousQueue也可以通过构造器参数来为其指定公平性。

  • 公平模式:SynchronousQueue采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者。
  • 非公平模式:SynchronoueQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者。

3.5. LinkedTransferQueue

LinkedTransferQueue是一个由链表数据结构构成的无界阻塞队列,由于该队列实现了TransferQueue接口,与其他阻塞队列相比主要有以下不同的方法:

  • transfer(E e) 如果当前有线程(消费者)正在调用take()方法或者可延时的poll()方法进行消费数据时,生产者线程可以调用transfer方法将数据传递给消费者线程。如果当前没有消费者线程的话,生产者线程就会将数据插入到队尾,直到有消费者能够进行消费才能退出;
  • tryTransfer(E e) tryTransfer方法如果当前有消费者线程(调用take方法或者具有超时特性的poll方法)正在消费数据的话,该方法可以将数据立即传送给消费者线程,如果当前没有消费者线程消费数据的话,就立即返回false。因此,与transfer方法相比,transfer方法是必须等到有消费者线程消费数据时,生产者线程才能够返回。而tryTransfer方法能够立即返回结果退出。
  • tryTransfer(E e,long timeout,imeUnit unit)
    与transfer基本功能一样,只是增加了超时特性,如果数据才规定的超时时间内没有消费者进行消费的话,就返回false。

3.6. LinkedBlockingDeque

LinkedBlockingDeque是基于链表数据结构的有界阻塞双端队列,如果在创建对象时为指定大小时,其默认大小为Integer.MAX_VALUE。与LinkedBlockingQueue相比,主要的不同点在于,LinkedBlockingDeque具有双端队列的特性。LinkedBlockingDeque基本操作如下图所示:
在这里插入图片描述
如上图所示,LinkedBlockingDeque的基本操作可以分为四种类型:1.特殊情况,抛出异常;2.特殊情况,返回特殊值如null或者false;3.当线程不满足操作条件时,线程会被阻塞直至条件满足;4. 操作具有超时特性。

另外,LinkedBlockingDeque实现了BlockingDueue接口而LinkedBlockingQueue实现的是BlockingQueue,这两个接口的主要区别如下图所示:
在这里插入图片描述
从上图可以看出,两个接口的功能是可以等价使用的,比如BlockingQueue的add方法和BlockingDeque的addLast方法的功能是一样的。

3.7. DelayQueue

DelayQueue是一个存放实现Delayed接口的数据的无界阻塞队列,只有当数据对象的延时时间达到时才能插入到队列进行存储。如果当前所有的数据都还没有达到创建时所指定的延时期,则队列没有队头,并且线程通过poll等方法获取数据元素则返回null。所谓数据延时期满时,则是通过Delayed接口的getDelay(TimeUnit.NANOSECONDS)来进行判定,如果该方法返回的是小于等于0则说明该数据元素的延时期已满。

package Concurrent.Blockingqueue;

import lombok.Data;

import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
 * @author xiejiahao
 * @version 1.0
 * @description: DelayQueue队列简单使用
     * DelayQueue 内部使用PriorityQueue存放数据,使用ReentrantLock实现线程同步。
     * 另外 队列中的元素要实现Delay接口,每个元素都有一个过期时间。
 * @date 2021/6/13 16:01
 */
public class DelayQueue_40 {
    @Data
    static class DelayElement implements Delayed {
        // 延迟时间
        private long delayTime;
        // 到期时间
        private long expire;
        //任务名称
        private String taskName;

        DelayElement(long delayTime, String taskName) {
            this.delayTime = delayTime;
            this.taskName = taskName;
            this.expire = System.currentTimeMillis() + this.delayTime;
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        @Override
        public int compareTo(Delayed o) {
            return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
        }
    }

    public static void main(String[] args) {
        DelayQueue<DelayElement> delayQueue = new DelayQueue<>();
        for (int i = 0; i < 10; i++) {
            DelayElement element = new DelayElement(ThreadLocalRandom.current().nextInt(100), "task: " + i);
            delayQueue.offer(element);
        }

        DelayElement element = null;
        try {
//            for (; ; ) {
                while ((element = delayQueue.take()) != null) {
                    System.out.println(element.toString());
                }
//            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4. 应用Demo

通过前面的学习,相比你已经对阻塞队列以及常用的类型有了一个基本的了解。 下面,南国将两个阻塞应用的最广泛的例子:生产者-消费者模式的实现,实现线程通信

4.1 实现生产者-消费者模式

1. 抛开这篇博客提到的阻塞队列,我们手动写一个非阻塞队列的方式实现消费者-生产者模式。

package 并发多线程.生产者_消费者模式;

import java.util.PriorityQueue;

/**
 * 使用Object.wait()和Object.notify()  非阻塞队列的方式实现消费者-生产者模式
 *
 * @author xjh 2019.12.26
 */
public class Wait_Notify {
    private int queueSize = 10;
    private PriorityQueue<Integer> queue = new PriorityQueue<>(queueSize);

    public static void main(String[] args) {
        Wait_Notify wait_notify = new Wait_Notify();
        Producer producer =wait_notify.new Producer();  //内部类的对象创建,需要通过外部类对象进行调用
        Consumer consumer=wait_notify.new Consumer();
        producer.start();
        consumer.start();
    }

    //创建内部类 Producer表示生产者线程相关的类
    class Producer extends Thread {
        @Override
        public void run() {
            produce();
        }

        private void produce() {
            while (true) {
                synchronized (queue) {
                    //对代码块进行加锁
                    while (queue.size() == queueSize) {    //队列已满,不能再生产了
                        System.out.println("the queue is full, please wait...");
                        try {
                            queue.wait();   //当前线程挂起,进入等待队列
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify(); //notify 唤醒等待挂起的线程
                        }
                    }
                    queue.offer(1); //入队一个元素
                    queue.notify();
                    System.out.println("I have inserted one element, the rest capacity is: " + (queueSize - queue.size()));
                }
            }
        }
    }

    //创建内部类 Consumer表示消费者线程相关的类
    class Consumer extends Thread {
        @Override
        public void run() {
            consume();
        }

        private void consume() {
            while (true) {
                synchronized (queue) {
                    //对代码块进行加锁
                    while (queue.size() == 0) {    //队列为空,不能再消费了
                        System.out.println("the queue is empty, please wait...");
                        try {
                            queue.wait();   //当前线程挂起,进入等待队列
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify(); //notify 唤醒等待挂起的线程
                        }
                    }
                    queue.poll(); //出队一个元素
                    queue.notify();
                    System.out.println("I have polled one element, the rest elements are: " + queue.size());
                }
            }
        }
    }
}

输出结果:

......
the queue is full, please wait...
I have polled one element, the rest elements are: 9
I have polled one element, the rest elements are: 8
I have polled one element, the rest elements are: 7
I have polled one element, the rest elements are: 6
I have polled one element, the rest elements are: 5
I have polled one element, the rest elements are: 4
I have polled one element, the rest elements are: 3
I have polled one element, the rest elements are: 2
I have polled one element, the rest elements are: 1
I have polled one element, the rest elements are: 0
the queue is empty, please wait...
I have inserted one element, the rest capacity is: 9
I have inserted one element, the rest capacity is: 8
I have inserted one element, the rest capacity is: 7
I have inserted one element, the rest capacity is: 6
I have inserted one element, the rest capacity is: 5
I have inserted one element, the rest capacity is: 4
I have inserted one element, the rest capacity is: 3
I have inserted one element, the rest capacity is: 2
I have inserted one element, the rest capacity is: 1
I have inserted one element, the rest capacity is: 0
.......

2. 使用阻塞队列实现生产者-消费者(这里我使用的是ArrayBlockingQueue)

package 并发多线程.生产者_消费者模式;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * 使用ArrayBlockQueue实现生产者消费者模式
 * @author xjh 2019.12.26
 */
public class ArrayBlockQueue_Demo {
    private int queueSize = 10;
    private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(queueSize);
    // 非阻塞模式用的PriorityQueue,阻塞模式下我们使用ArrayBlockingQueue

    public static void main(String[] args) {
        ArrayBlockQueue_Demo arrayBlockQueue_demo = new ArrayBlockQueue_Demo();
        Producer producer=arrayBlockQueue_demo.new Producer();
        Consumer consumer=arrayBlockQueue_demo.new Consumer();

        consumer.start();
        producer.start();
    }

    //创建内部类 Producer表示生产者线程相关的类
    class Producer extends Thread {
        @Override
        public void run() {
            produce();
        }

        private void produce() {
            while (true) {
                try {
                    queue.put(1);
                    System.out.println("I have inserted one element, the rest capacity is: " + (queueSize - queue.size()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //创建内部类 Consumer表示消费者线程相关的类
    class Consumer extends Thread {
        @Override
        public void run() {
            consume();
        }

        private void consume() {
            while (true) {
                try {
                    queue.take();
                    System.out.println("I have took one element, the rest elements are: " + queue.size());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

输出结果:

.........
I have took one element, the rest elements are: 9
I have took one element, the rest elements are: 8
I have took one element, the rest elements are: 8
I have took one element, the rest elements are: 7
I have took one element, the rest elements are: 6
I have took one element, the rest elements are: 5
I have took one element, the rest elements are: 4
I have took one element, the rest elements are: 3
I have took one element, the rest elements are: 2
I have took one element, the rest elements are: 1
I have took one element, the rest elements are: 0
I have inserted one element, the rest capacity is: 1
I have inserted one element, the rest capacity is: 9
I have inserted one element, the rest capacity is: 9
I have inserted one element, the rest capacity is: 8
I have inserted one element, the rest capacity is: 7
I have inserted one element, the rest capacity is: 6
I have inserted one element, the rest capacity is: 5
I have inserted one element, the rest capacity is: 4
I have inserted one element, the rest capacity is: 3
I have inserted one element, the rest capacity is: 2
I have inserted one element, the rest capacity is: 1
I have inserted one element, the rest capacity is: 0
.........

注意,这两段代码的结果都是截取的部分效果。 读者相比发现了使用阻塞队列代码要简单得多,不需要再单独考虑同步和线程间通信的问题。
在并发编程中,一般推荐使用阻塞队列,这样实现可以尽量地避免程序出现意外的错误。

阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。还有其他类似的场景,只要符合生产者-消费者模型的都可以使用阻塞队列。

4.2 阻塞队列实现线程通信(这里我使用的是LinkedBlockingQueue)

package 并发多线程;

import java.util.concurrent.LinkedBlockingQueue;

/**
 * 使用BlockingQueue来实现线程通信
 * @author xjh 2019.09.09
 * 这里我用了两种玩法:
一种是共享一个queue,根据peek和poll的不同来实现;
第二种是两个queue,利用take()会自动阻塞来实现。
 */
class MethodSeven {
    //1.共享一个queue,根据peek和poll的不同来实现;
    private final LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();

    public Runnable newThreadOne() {
        final String[] inputArr = Helper.buildNoArr(52);
        return new Runnable() {
            private String[] arr = inputArr;

            public void run() {
                for (int i = 0; i < arr.length; i = i + 2) {
                    Helper.print(arr[i], arr[i + 1]);
                    queue.offer("TwoToGo");
                    while (!"OneToGo".equals(queue.peek())) {
                    }
                    queue.poll();
                }
            }
        };
    }

    public Runnable newThreadTwo() {
        final String[] inputArr = Helper.buildCharArr(26);
        return new Runnable() {
            private String[] arr = inputArr;

            public void run() {
                for (int i = 0; i < arr.length; i++) {
                    while (!"TwoToGo".equals(queue.peek())) {
                    }
                    queue.poll();
                    Helper.print(arr[i]);
                    queue.offer("OneToGo");
                }
            }
        };
    }

    //2.两个queue,利用take()会自动阻塞来实现。
    private final LinkedBlockingQueue<String> queue1 = new LinkedBlockingQueue<>();
    private final LinkedBlockingQueue<String> queue2 = new LinkedBlockingQueue<>();

    public Runnable newThreadThree() {
        final String[] inputArr = Helper.buildNoArr(52);
        return new Runnable() {
            private String[] arr = inputArr;

            public void run() {
                for (int i = 0; i < arr.length; i = i + 2) {
                    Helper.print(arr[i], arr[i + 1]);
                    try {
                        queue2.put("TwoToGo");
                        queue1.take();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
    }

    public Runnable newThreadFour() {
        final String[] inputArr = Helper.buildCharArr(26);
        return new Runnable() {
            private String[] arr = inputArr;

            public void run() {
                for (int i = 0; i < arr.length; i++) {
                    try {
                        queue2.take();
                        Helper.print(arr[i]);
                        queue1.put("OneToGo");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
    }
}

//创建一个枚举类型
enum Helper {
    instance;
    private static final ExecutorService tPool = Executors.newFixedThreadPool(2);
    //数字
    public static String[] buildNoArr(int max) {
        String[] noArr = new String[max];
        for(int i=0;i<max;i++){
            noArr[i] = Integer.toString(i+1);
        }
        return noArr;
    }
    //字母
    public static String[] buildCharArr(int max) {
        String[] charArr = new String[max];
        int tmp = 65;
        for(int i=0;i<max;i++){
            charArr[i] = String.valueOf((char)(tmp+i));
        }
        return charArr;
    }

    public static void print(String... input){
        if(input==null)
            return;
        for(String each:input){
            System.out.print(each);
        }
    }
    public void run(Runnable r){
        tPool.submit(r);
    }
    public void shutdown(){
        tPool.shutdown();
    }
}
public class BlockingQueueTest {
    public static void main(String args[]) throws InterruptedException {
        MethodSeven seven = new MethodSeven();
        Helper.instance.run(seven.newThreadOne());
        Helper.instance.run(seven.newThreadTwo());
        Thread.sleep(2000);
        System.out.println("");
        Helper.instance.run(seven.newThreadThree());
        Helper.instance.run(seven.newThreadFour());
        Helper.instance.shutdown();
    }
}

输出结果:

12A34B56C78D910E1112F1314G1516H1718I1920J2122K2324L2526M2728N2930O3132P3334Q3536R3738S3940T4142U4344V4546W4748X4950Y5152Z
12A34B56C78D910E1112F1314G1516H1718I1920J2122K2324L2526M2728N2930O3132P3334Q3536R3738S3940T4142U4344V4546W4748X4950Y5152Z

4.3 logback 异步打印日志中ArrayBlockingQueue的使用

(2021-06更新)
logback的异步日志模型是一个多生产者和单消费者的模型, 其通过使用队列把同步日志打印转换成异步,业务线程只需调用异步appender把日志任务放入队列,而日志线程则负责使用同步的appender进行具体的日志打印。日志打印线程只负责生产日志并将其放入队列,不关心消费线程何时把日志jurisdiction写入磁盘。
其中AsyncAppender 是实现异步日志的关键。
例如,logback.xml配置中出现

<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
		<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
		<discardingThreshold>0</discardingThreshold>
		<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
		<queueSize>512</queueSize>
		<!-- 添加附加的appender,最多只能添加一个 -->
		<appender-ref ref="FILE" />
	</appender>

追踪源代码

public class AsyncAppender extends AsyncAppenderBase<ILoggingEvent>
...
public class AsyncAppenderBase<E> extends UnsynchronizedAppenderBase<E> implements AppenderAttachable<E> {
    AppenderAttachableImpl<E> aai = new AppenderAttachableImpl();
    BlockingQueue<E> blockingQueue;
    public static final int DEFAULT_QUEUE_SIZE = 256;
    int queueSize = 256;
    int appenderCount = 0;
    static final int UNDEFINED = -1;
    int discardingThreshold = -1;
    boolean neverBlock = false;
    AsyncAppenderBase<E>.Worker worker = new AsyncAppenderBase.Worker();
    public static final int DEFAULT_MAX_FLUSH_TIME = 1000;
    int maxFlushTime = 1000;
     .....
     public void start() {
        if (!this.isStarted()) {
            if (this.appenderCount == 0) {
                this.addError("No attached appenders found.");
            } else if (this.queueSize < 1) {
                this.addError("Invalid queue size [" + this.queueSize + "]");
            } else {
                this.blockingQueue = new ArrayBlockingQueue(this.queueSize);
                if (this.discardingThreshold == -1) {
                    this.discardingThreshold = this.queueSize / 5;
                }

                this.addInfo("Setting discardingThreshold to " + this.discardingThreshold);
                this.worker.setDaemon(true);
                this.worker.setName("AsyncAppender-Worker-" + this.getName());
                super.start();
                this.worker.start();
            }
        }
    }
   }

由此可以看到 logback使用的是有界队列ArrayBlockingQueue 之所以设计成为有界队列主要是考虑内存溢出问题。在高并发下写日志的QPS很高,若设置为无界队列,队列自身占用很大的内存,容易出现OOM。

参考资料:

  1. 并发容器之BlockingQueue
  2. Java并发编程:阻塞队列
  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值