引言:之前上一篇 java线程池复用原理 详细解释了线程池复用原理,其中一个关键就是阻塞队列,这一篇将会详细介绍阻塞队列的特征。
阻塞队列是什么?和普通队列的区别是什么
阻塞队列对应的类名叫java.util.concurrent.BlockingQueue,它的父类是java.util.Queue,都是接口,而Queue还继承了Collection接口,所以具备size() isEmpty() contains() iterator()等方法
Queue
对于Queue我们比较熟悉,队列是一个先进先出的数据结构,可以往队尾加入元素,从队头获取或弹出一个元素,常见的方法有:
1、boolean add(E element) 往队尾插入元素,如果超出capacity,则抛出异常
2、boolean offer(E element) 往队尾插入元素,成功返回true,失败(capacity restrictions)返回false
3、E pool() 从对头弹出一个元素,如果队列为空,则返回null。
4、E peek() 获取队头元素,但不弹出,如果队列为空,则返回null
比较常见的一个实现就是LinkedList(无界队列)。详细使用可见下图
BlockingQueue
对于阻塞队列BlockingQueue,因为继承了Queue接口,所以具备Queue的先进先出属性,但是也对一系列方法进行了增加了重写,常见方法有:
1、boolan add(E element) 往队尾插入元素,如果超出capacity,则抛出异常 注意,此方法不会阻塞 ps:和Queue方法含义一致
2、boolean offer(E element) 往队尾插入元素,成功返回true,失败(capacity restrictions)返回false 注意,此方法不会阻塞 ps:和Queue方法含义一致
3、void put(E element) 将一个元素加入到队列中,阻塞到直至加入成功为止,因为阻塞的语义,此方法可能抛出InterruptedException.
4、boolean offer(E element,long timeout,TimeUnit unit), 类似于put方法,但是只会阻塞指定时间,到时候没插入成功,则返回false
5、E take() 获取并弹出队头元素,阻塞直到成功为止
6、E poll(long timeout,TimeUnit uint) 获取并弹出队头元素,只会阻塞指定时间,如果时间到,则返回null
显然,3、4、5、6都是含有阻塞含义的方法,需要重点注意。
BlockingQueue实现类是LinkedBlockingQueue
通过阻塞队列如何实现线程复用?
我们回到线程池是如何通过阻塞队列实现复用上,我们知道,线程池的线程通过while()死循环,并阻塞在getTask()方法上来实现线程复用,所以我们继续分析getTask()方法
从上图可以知道,如果执行getTask时,当前时刻所有线程池的线程数不大于corePoolSize,那么当前执行getTask()方法的线程就会调用take()方法,一直阻塞,直到获取到任务后,才返回getTask()方法;而当前大于了corePoolSize,则调用poll()方法,只会阻塞keepAlive时间,如果仍旧没有获取到任务,则会返回getTask()方法为null并退出while死循环,最终该非核心线程会被ThreadPoolExecutor#processWorkerExit方法回收掉。PS:这里注意一个点,线程池中的线程并没有说最先创建出来的线程就是核心线程,后创建的就是非核心线程的说法,完全有可能threadpool-thread-1先被创建出来,但后来threadpool-thread-1执行getTask方法时候,判断到wokerCount > corePoolSize,那么这个线程虽然是第一个被创建出来的线程,也是会被回收掉。也就是说,核心线程是一个数量值,而不是一个boolean标志绑定在每个Worker上的。
take、poll等方法如何实现阻塞等待?
我们来分析一下LinkedBlockingQueue的源码,需要有AQS和Condition等基本了解,这些都是java并法包中的基础,如果了解Object wait() notify()这种线程间通讯的方式的话,理解起来并不难,可参考:AQS详解
LinkedBlockingQueue中有两个Lock和两个Condition变量
/** 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();
当往线程池中添加任务,workQueue.offer(command)时,执行一下代码:
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}
当线程池的线程getTask()方法中执行workQueue.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;
}
从上述分析可知,offer和take使用的是两把锁,分别有自己的等待队列Condition。
offer方法源码分析:
1、如果添加元素时,已经等于队列的长度,此时不会阻塞当前线程,而是直接返回false,这也是线程池中队列满了之后,会直接创建非核心线程的关键所在
2、如果没达到容量,则需要先获取putLock,再次判断是否小于容量capacity,true时会将当前元素加入到队列中,并且调用notFull.signal()方法,告知其他的生产者,进行生产(主要是因为有些生产者可能调用put方法阻塞了,需要其他线程来进行唤醒操作)
3、最后调用if(c == 0) signalNotEmpty(); 注意这里c是getAndIncrement(),这种情况在什么时候发生?在当前队列为空时,当前生产者提交了一个任务,所以需要唤醒一个阻塞在本队列中其他消费者线程进行消费。这一步至关重要,因为线程池中的线程执行完firstTask任务后,就会调用workQueue.take()方法,如果队列中没任务则会一直阻塞,那么什么时候会被唤醒? 就是在此情况下,生产者生产了一个任务到队列时,由生产者线程来唤醒阻塞在队列上的消费者线程(当然了,take方法如果判断到队列元素大于1的话,本身也会唤醒其他消费者线程)
take方法源码分析:
1、尝试从队列中获取一个元素,如果队列为空,则当前消费者线程阻塞
2、如果当前队列不为空,那么从队列中弹出一个元素
3、判断当前容量是否大于1,如果大于1,需要调用notEmpty.signal()唤醒其他的消费者线程进行消费
4、如果c==capacity,注意这里是c = getAndDecrement,所以此时是队列元素 == capacity-1, 需要唤醒生产者线程进行生产(c < capacity-1的其他情况时,生产者线程会自行唤醒其他的生产者线程进行生产,无需消费者线程操心)