目录
阻塞队列简介
介绍
介绍阻塞队列的定义、种类、实现原理和使用场景有利于大家更好理解线程池。
阻塞队列一般用于生产者和消费者场景:生产者在队列只添加线程,消费者只取出线程。队列就是这样一种元素存取的容器。
阻塞队列满足以下必要条件:
- 队列数据为空,消费者端所有线程都会被阻塞(挂起),直到有数据放入队列。
- 在队列填满数据下,生产者端所有线程都会被阻塞(挂起),直到队列有空位。
BlockingQueue 阻塞队列核心方法
阻塞队列用到的类大多是BlockingQueue的派生类,BlockingQueue只是java.util.concurrent
包中的一个接口,但我们还是要说说其中核心方法:
放入数据
- offer(Object): 将Object放入队列,可以容纳则返回true,否则false,本方法不阻塞当前执行方法线程。
- offer(Object,long,TimeUnit):比上方法加了等待时间参数,在指定时间内无法加入元素,则返回失败。
- put(Object):将Object加入队列,若没有空间,此方法被线程阻断,直到有空间才能继续。
获取数据
- poll(long, TimeUnit):取出队首的对象。如果有数据可取,则立刻返回数据;若超时没有数据,返回失败。
- take():取出队首的对象,为空进行等待直到取出。
- drainTo():取出全部可用数据(可指定数量),多个数据批量取出有利于提升数据获取率。
阻塞队列实现类
ArrayBlockingQueue
由数组结构组成的有界队列(FIFO 先进先出)。在默认情况不保证公平访问队列(公平访问队列
:阻塞所有生产者或消费者线程,当队列可用,可以按照阻塞的先后顺序访问队列)。
通常情况下,为了保证公平性会减低吞吐量。
LinkedBlockingQueue
链表结构组成的有界阻塞队列,与ArrayBlockingQueue
不同的是,LinkedBlockingQueue
能高效处理并发数据,是因为生产者和消费者端分别用了独立的锁控制数据同步,这也意味着在高并发情况下,两端可以并行操作队列的数据,以此提高性能。
注意,如果没有指定大小,会构建一个无限大的容量限制,这是很危险的,有耗尽内存的风险。
一般来说,用上述两个方法足矣。
PriorityBlockingQueue
支持优先级排序的无界阻塞队列,默认优先级越高越快被取出,可以自定义实现compareTo
或指定构造参数Comparator
对元素进行排序,但不保证同优先级的先后顺序
DelayQueue
支持延时队列获取元素的无界阻塞队列,队列使用PriorityBlockingQueue
来实现,队列元素必须实现Delayed
接口,创建元素时,可指定元素到期时间,在到期前才能取走。
SynchronousQueue
不存储元素的阻塞队列。每个插入操作必须等待另一个线程的移除操作,删除操作也是如此。由于没有容器,peek
操作自然也无法使用。
LinkedTransferQueue
链表结构组成的无界阻塞队列:他可以算是LinkedBolckingQueue
和 SynchronousQueue
和合体。我们知道 SynchronousQueue
内部无法存储元素,当要添加元素的时候,需要阻塞,不够完美,LinkedBolckingQueue
则内部使用了大量的锁,性能不高。而他继承两者性能又高,又不阻塞的特点。
LinkedBlockingDeque
链表结构组成的双向阻塞队列:该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(插入/删除);并且,该阻塞队列是支持线程安全。
此外,LinkedBlockingDeque还是可选容量的(防止过度膨胀),即可以指定队列的容量。如果不指定,默认容量大小等于Integer. MAX_VALUE
。
阻塞队列使用场景
除了线程池的实现使用阻塞队列外,我们还可以在生产者-消费者模式中使用阻塞队列:非阻塞队列实现生产者-消费者模式无需单独考虑同步和线程之间通信问题,实现起来更为简单。
线程池
简介
在进行大量异步计算、使用太多线程,会使得线程的创建和销毁浪费大量资源,并且难以控制,这就产生了线程池创建的条件。
Java 1.5就提供了Executor
框架用于把任务的提交和执行解耦。Executor
核心成员就是ThreadPoolExecutor
,是线程池的核心类。我们就以此展开。
ThreadPoolExecutor
我们先看看最原始的构造方法,也是最多参数的构造方法,上面展示了取值范围▲
- corePoolSize :核心线程数。默认线程池没有线程,只有在任务提交到时,才会创建线程来处理任务;若
线程数 < corePoolSize
,创建新的线程。若线程数 >= corePoolSize
;则不再创建新线程,此时会加入到指定队列(见参数workQueue
)。注意: 若调用prestartAllCoreThreads()
则会提前创建并启用所有核心线程来等待任务。 - maximumPoolSize: 允许创建的最大线程数。
- keepAliveTime: 非核心线程闲置的超时时间。超时则非核心线程被回收,如果任务过多且执行时机短,可提高
keepAliveTime
时间来保证线程利用率。此外,设置allowCoreThreadTimeOut(true)
,也会作用到核心线程上。 - TimeUnit: 上面参数单位。
- workQueue:
线程数 >= corePoolSize
;则不再创建新线程,此时会加入到此指定队列中,只要是继承于此的类即可。 - ThreadFactory: 线程工厂。可以用线程工厂给每个创建的线程设置名字,一般可不设置。
- RejectedExecutionHandler: 饱和策略。当队列和线程池满时,多余的任务采取的处理策略。默认是
AbortyPolicy
,表示不处理并抛出异常。
此外还有三种策略:
CallerRunsPolicy:
调用所在线程处理任务。此策略提供简单反馈控制机制,能减缓新任务的提交速度。DiscardPolicy:
不执行任务,并删除。DiscardOldestPolicy:
丢弃队列最近的任务,并执行当前的任务。
一般处理流程
逻辑流程图▼
原理图则是(错误修正:线程数 < corePoolSize
改为线程数 >= corePoolSize
):
线程池的种类
通过直接或间接地配置ThreadPoolExecutor
的参数可以创建不同类型的ThreadPoolExecutor
,其中4种常用,我们以此介绍。
FixedThreadPool
可重用固定线程数的线程池,在Executors
类提供了创建FixedThreadPool
的方法,如下所示:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
可见:
maximumPoolSize
和 corePoolSize
都为同一需要指定的参数,这说明:只有核心线程,且数量固定。
keepAliveTime
= 0L,这也意味着多余的线程立刻终止
LinkedBlockingQueue
: 采用无阻塞队列(默认无限)
它的基本执行过程如下
- 线程数少于
corePoolSize
, 会立刻创建新线程执行任务。 - 线程数到达
corePoolSize
后,任务则加入到LinkedBlockingQueue
中。 - 当线程执行有空缺时,会循环从
LinkedBlockingQueue
中获取任务来执行。
FixedThreadPool
使用了LinkedBlockingQueue
, 也就是无界队列(队列最大可容纳Integer.MAX_VALUE), 因此会造成以下影响:
- 线程池线程数到达
corePoolSize
后,任务会被存放在LinkedBlockingQueue
中 - 因为无界队列,只要未调用
shutdown()
或者shutdownNow()
方法,就不会拒绝任意数量任务。
CachedThreadPool
代码如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
这意味着,没有核心线程,非核心线程无界,线程最多等待60s,由于使用了SynchronousQueue
,所以每个插入操作都必须等待另一个线程移除后才执行。同理,任何一个移除操作都等待另一个插入操作。
当执行execute
方法时,首先执行SynchronousQueue
的offer方法提交任务,并查询线程池是否有空闲的线程执行SynchronousQueue
的poll方法来移除任务。如果有则配对成功,将任务交个空线程处理;没有则配对失败,创建新线程去处理任务。当线程池空闲时,会执行SynchronousQueue
的poll
方法,等待SynchronousQueue
提交新的任务。超时则空线程终止。
因为无界,如果提交的任务大于线程池中线程处理的任务速度,就会不断创建新线程。此外,提交的任务都立刻有线程处理,所以适合大量需要处理并耗时少的任务。
SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
只有一个核心线程,其他参数与FixedThreadPool
一样,这里就不赘述了。
ScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
ScheduledThreadPool
是一个能实现定时、周期性任务的线程池。构造方法最后调用的是ThreadPoolExectuor
构造方法。因为采用的是无界DelayedWorkQueue
,所以Integer.MAX_VALUE
没有实际意义。
具体用法可见: 线程池之ScheduledThreadPool学习