阻塞队列与线程池

207 阅读4分钟

阻塞队列

简介

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。

BlockQueue的核心方法:

  • offer(anObject)
  • offer(E o, long timeout, TimeUnit unit)
  • put(anObject)
  • poll(time)
  • poll(long timeoout, TimeUnit unit)
  • take()
  • drainTo()

Java中的阻塞队列

队列解释
ArrayBlockingQueue由数组结构组成的有界阻塞队列
LinkedBlockingQueue由链表结构组成的有界阻塞队列
PriorityBlockingQueue支持优先级排序的无界阻塞队列
DelayQueue使用优先级队列实现的无界阻塞队列
SynchronousQueue不存储元素的阻塞队列
LinkedTransferQueue由链表结构组成的无界阻塞队列
LinkedBlockingDeque由链表结构组成的双向阻塞队列

阻塞队列的实现原理

ArrayBlockingQueue为例,我们先来看看代码,如下所示:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = -817911632652898426L;

    /** 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;
}

接下来我们来看一下put方法:

public void put(E e) throws InterruptedException {
    Objects.requireNonNull(e);
    //获取锁
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        //如果元素已满则进行等待
        while (count == items.length)
            notFull.await();
        //当线程被其它线程唤醒则插入元素
        enqueue(e);
    } finally {
        //解锁
        lock.unlock();
    }
}

接下来看看enqueue(e)方法:

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唤醒正在等待取元素的线程
    notEmpty.signal();
}

再来看看take方法:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        //如果队列为空则进行等待
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}

跟put方法实现类似,put方法等待的是notFull信号,而take方法等待的是notEmpty信号。 下面是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;
}

阻塞队列的使用场景

阻塞队列的一个常见的使用场景就是生产者-消费者模式,首先不使用阻塞队列实现,代码如下:

/*
使用阻塞队列实现生产者消费模式
 */
public class customerToProducer {
    private int queueSize = 3;
    private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
    public static void main(String[] args) throws InterruptedException {
        customerToProducer cp = new customerToProducer();
        Producer producer = cp.new Producer();
        Consumer consumer = cp.new Consumer();
        producer.start();
        consumer.start();
    }
    //消费者类
    class Consumer extends Thread{
        @Override
        public void run() {
            while(true){
                synchronized(queue){
                    while(queue.size()==0){
                        try{
                            System.out.println("队列空,等待数据");
                            queue.wait();
                        }catch(InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                    //每次移走队列元素
                    queue.poll();
                    queue.notify();
                    System.out.println("移走元素成功!");
                    System.out.println("剩余:"+queue.size());
                }
            }
        }
    }

    //生产者类
    class Producer extends Thread{
        @Override
        public void run() {
            while(true){
                synchronized(queue){
                    while(queue.size()==queueSize){
                        try{
                            System.out.println("队列满,等待空余空间");
                            queue.wait();
                        }catch(InterruptedException e){
                            e.printStackTrace();
                            queue.notify();
                        }
                    }
                    //每次插入一个元素
                    queue.offer(1);
                    queue.notify();
                    System.out.println("插入元素成功!");
                    System.out.println("剩余:"+queue.size());
                }
            }
        }
    }
}

下面是使用阻塞队列实现的生产者-消费者模式:

public class customerToProducer {

    private int queueSize = 10;
    private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(queueSize);
    public static void main(String[] args) throws InterruptedException {
        customerToProducer1 cp = new customerToProducer();
        Producer producer = cp.new Producer();
        Consumer consumer = cp.new Consumer();
        producer.start();
        consumer.start();
    }
    //消费者类
    class Consumer extends Thread{
        @Override
        public void run() {
            while(true){
                try{
                    queue.take();
                    System.out.println("移走元素成功!");
                    System.out.println("剩余:"+queue.size());
                    this.sleep(1000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }

    //生产者类
    class Producer extends Thread{
        @Override
        public void run() {
            while(true){
                try{
                    queue.put(1);
                    System.out.println("插入元素成功!");
                    System.out.println("剩余:"+queue.size());
                    this.sleep(1000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
}

很显然,使用阻塞队列实现无须单独考虑同步和线程间通信的问题,其实现起来很简单。

线程池

在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销。如果每次执行一个任务都需要开一个新线程去执行,则这些线程的创建和销毁将消耗大量的资源;并且线程都是“各自为政”的,很难对其进行控制,更何况有一堆的线程在执行。这时就需要线程池来对线程进行管理。

ThreadPoolExecutor

可以通过ThreadPoolExecutor来创建一个线程池,ThreadPoolExecutor类一共有4个构造方法。其中,拥有最多参数的构造方法如下所示:

public ThreadPoolExecutor(
    //核心线程数
    int corePoolSize,
    //线程池允许创建的最大线程数
    int maximumPoolSize,
    //非核心线程闲置的超时时间
    long keepAliveTime,
    //keepAliveTime参数的时间单位
    TimeUnit unit,
    //任务队列
    BlockingQueue<Runnable> workQueue,
    //线程工厂
    ThreadFactory threadFactory,
    //饱和策略
    RejectedExecutionHandler handler) {
    ...
}

线程池的处理流程和原理

上图介绍了线程池的处理流程,但还不是很直观。结合下图,我们就能更好地了解线程池的原理:

线程池的种类

  1. FixedThreadPool

FixedThreadPool 是可重用固定线程数的线程池:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

FixedThreadPool只有核心线程,并且数量是固定的,没有非核心线程。

5.PNG

  1. CachedThreadPool

CachedThreadPool是一个根据需要创建线程的线程池:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

CachedThreadPool没有核心线程,非核心线程是无界的。

6.PNG

  1. SingleThreadExecutor

SingleThreadExecutor是使用单个工作线程的线程池:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

SingleThreadExecutor只有一个核心线程。

7.PNG

  1. ScheduledThreadPool

ScheduledThreadPool是一个能实现定时和周期性任务的线程池:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

这里创建了ScheduledThreadPoolExecutor,ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,它主要用于给定延时之后的运行任务或者定期处理任务。ScheduledThreadPoolExecutor 的构造方法如下:

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }

ScheduledThreadPoolExecutor的execute方法的执行示意图如下:

8.PNG DelayedWorkQueue会将任务进行排序,先要执行的任务放在队列的前面。

当执行完任务后,会将ScheduledFutureTask中的time变量改为下次要执行的时间并放回到DelayedWorkQueue中。