线程池中的阻塞队列

372 阅读5分钟

前言

线程池是实现异步的重要手段之一,而根据《阿里巴巴java开发规范》,创建线程只能用线程池,而且这个线程池必须是通过ThreadPoolExecutor来构造。而线程池的AQS并不是线程池的某个成员变量,我们简单的过一遍线程池的参数和执行流程。

一、线程池核心参数

  1. 线程池的核心线程数量:int corePoolSize
  2. 线程池的最大线程数:int maximumPoolSize
  3. 临时存活的最长时间:long keepAliveTime
  4. 时间单位:TimeUnit unit
  5. 任务队列,用来储存等待执行任务的队列:BlockingQueue workQueue
  6. 线程工厂,用来创建线程,一般默认即可:ThreadFactory threadFactory
  7. 拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务:RejectedExecutionHandler handler

二、线程池拒绝策略

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolExecutor 定义一些策略:

拒绝策略说明
AbortPolicy默认的拒绝策略,会抛出 RejectedExecutionException 异常。
CallerRunsPolicy在任务被拒绝添加后,会在调用者线程中执行该任务。
DiscardPolicy直接丢弃被拒绝的任务,不做任何处理。(不推荐)
DiscardOldestPolicy丢弃等待队列中最久的任务,并将当前任务加入队列

三、线程池处理任务的流程

当我们要使用一个线程池来执行某一个线程的时候,我们通常会调用executesubmit方法,其中submit的底层也是execute,可以认为execute是线程池中最重要的方法,而execute方法的实现关键在于底层的addWorker方法,execute的执行分为三步:

  1. 判断当前正在运行的线程数,如果当前线程数小于核心线程数,就调用addWorker方法创建一个线程来执行当前任务。这个过程中,addWorker会做更深的判断,来决定是否可以创建线程

  2. 如果任务可以成功加入队列,再重新判断一次线程池的状态,再决定可不可以加入队列

  3. 如果无法无法入队就调用addWorker方法创建一个新的线程,如果失败就执行拒绝策略

(图源:JavaGuide) image.png

四、阻塞队列

阻塞队列是线程池的一个参数,也是线程池的核心部分。 线程池中可以使用的阻塞队列有很多种,通过下图介绍一下阻塞队列的成员(图源:Java线程池实现原理及其在美团业务中的实践):

image.png

其中要补充的还有DelayedWorkQueue,是ScheduledThreadPoolExecutor中使用的阻塞队列。

这些队列中,通常长度为无界的或者是int的最大值的阻塞队列都不推荐使用,原因是容易造成OOM,以SynchronousQueue为例,这个队列的长度为0,被线程池CachedThreadPool使用。CachedThreadPool中没有核心线程,线程数的最大值是int类型的最大值,当一个任务进来时,如果没有空闲的线程就会立即创建一个,在极端情况下带来的开销会更大。

下面来看一个简单的阻塞队列的实现:

public class BlockingQueue<T> {
    //双向队列,两个条件变量:获取元素时队列为空要等待,添加元素时队列满了要等待
    private Deque<T> queue = new ArrayDeque<>();
    private int capacity = 5;
    private ReentrantLock lock = new ReentrantLock();
    private Condition emptyCondition = lock.newCondition();
    private Condition fullCondition = lock.newCondition();

    //获取元素
    public T take(){
        lock.lock();
        try {
            while (queue.isEmpty()){
                //队列为空阻塞
                try {
                    emptyCondition.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            //不为空
            T t = queue.removeFirst();
            fullCondition.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    //添加元素
    public void put(T element){
        lock.lock();
        try {
            while(queue.size() == capacity){
                try {
                    fullCondition.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            queue.addLast(element);//添加到队尾
            emptyCondition.signal();
        } finally {
            lock.unlock();
        }
    }

    public int size(){
        lock.lock();
        try {
            return queue.size();
        } finally {
            lock.unlock();
        }
    }
}

大部分阻塞队列的获取和添加元素的API都是这样实现的,可能有些人看到这段代码会思考一个问题:如果获取元素时队列为空阻塞了,那岂不是死锁了?

其实非也,这里就要说到一个经常和ReentrantLock配合使用的类Condition了,Condition是一个接口,他的实现类ConditionObjectAQS中的一个类,本质上也是AQS的实现。如果调用了这个类的await方法,当前线程会释放锁并进入一个等待队列,这个等待队列就是AQSConditionObject中显示地维护了队头指针和队尾指针,当调用signal方法的时候就会唤醒队头节点的线程并获取锁,这就是不会死锁的原因。

五、LinkedBlockingQueue的好处

在spring框架中,ThreadPoolTaskExecutor中有一段代码:

protected BlockingQueue<Runnable> createQueue(int queueCapacity) {
    return (BlockingQueue)(queueCapacity > 0 ? new LinkedBlockingQueue(queueCapacity) : new SynchronousQueue());
}

通过这段代码可以看到,ThreadPoolTaskExecutor会根据我们传入的队列大小对ThreadPoolExecutor的BlockingQueue进行赋值,队列大小大于0时为LinkedBlockingQueue,队列大小为0时则是SynchronousQueue。其实我们可以看做ThreadPoolTaskExecutor的阻塞队列默认就是LinkedBlockingQueue

那为什么我们前面说不推荐使用无界的阻塞队列,而spring的ThreadPoolTaskExecutor却用了无界的LinkedBlockingQueue?我们从LinkedBlockingQueue和ArrayBlockingQueue的区别来进行说明:

LinkedBlockingQueue和ArrayBlockingQueue的区别

  1. 在LinkedBlockingQueue中提供了两个ReentrantLock的锁对象:putLock和takeLock,其实就是逻辑上的写锁和读锁,这样就实现了读写分离。
  2. 而ArrayBlockingQueue是顺序存储的循环队列,但是由于是顺序存储,读写同时进行的时候无法保证线程安全,所以在ArrayBlockingQueue只有一个ReentrantLock的锁对象,无论是读操作还是写操作都是对这个数组进行加锁,相比LinkedBlockingQueue来说ArrayBlockingQueue的性能就差了很多。
  3. 再者,虽然LinkedBlockingQueue是无界的,ArrayBlockingQueue是有界的,但是经过spring的封装,用成员变量queueCapacity实现了LinkedBlockingQueue的逻辑有界。

综上,LinkedBlockingQueue成为了线程池的不二之选。

总结

线程池中的阻塞队列使用了AQS机制,阻塞队列通过ReentrantLock和Condition配合实现,其中AQS则是实现于ConditionObject的条件队列中。而至于为什么选择LinkedBlockingQueue,则是因为他的高性能带来的高吞吐量的能力,spring中的ThreadPoolTaskExecutor实现了LinkedBlockingQueue的逻辑有界,当我们没有使用spring框架的时候也可以自己实现。