@[TOC](BlockingQueue && Executor)
#BlockingQueue和Executor在我们的多线程开发中,使用时非常广泛的,阻塞队列我们一般可以单独使用,但是一旦使用了线程池Executor,基本都会用到阻塞队列相关功能,所以在这里把两者的基本原理和使用方式做简单的介绍。
什么是阻塞队列
1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,
直到队列不满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队
列变为非空。
在并发编程中使用 生产者和消费者模式能够解决绝大多数并发问题。该模式
通过平衡生产线程和消费线程的工作能力来提高程序整体处理数据的速度。
常用阻塞队列
·ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
·DelayQueue:一个使用优先级队列实现的无界阻塞队列。
·SynchronousQueue:一个不存储元素的阻塞队列。
·LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
从源码可以了解到 以上的阻塞队列都实现了 BlockingQueue 接口,也都是线程安全的
ArrayBlockingQueue
是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证线程公平的访问队列,但是初始化的时候可以设置。
LinkedBlockingQueue
是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为
Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。
Array 实现和 Linked 实现的区别
为什么要用线程池?
Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行
任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来 3
个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
ThreadPoolExecutor 的类关系
Executor 是一个接口,它是 Executor 框架的基础,它将任务的提交与任务的执行分离开来。
ExecutorService 接口继承了 Executor,在其上做了一些 shutdown()、submit()的扩展,可以说是真正的线程池接口;
AbstractExecutorService 抽象类实现了 ExecutorService 接口中的大部分方法;
ThreadPoolExecutor 是线程池的核心实现类,用来执行被提交的任务。
ScheduledExecutorService 接口继承了 ExecutorService 接口,提供了带"周期执行"功能 ExecutorService;
ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。ScheduledThreadPoolExecutor 比 Timer 更灵活,功能更强大。
线程池的创建各个参数含义:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
corePoolSize
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于 corePoolSize;
如果当前线程数为 corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
如果执行了线程池的 prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于 maximumPoolSize
keepAliveTime
线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于 corePoolSize 时才有用
TimeUnit
keepAliveTime 的时间单位
workQueue
workQueue 必须是 BlockingQueue 阻塞队列。当线程池中的线程数超过它的
corePoolSize 的时候,线程会进入阻塞队列进行阻塞等待。通过 workQueue,线程池实现了阻塞功能
workQueue
用于保存等待执行的任务的阻塞队列,一般来说,我们应该尽量使用有界队列,因为使用无界队列作为工作队列会对线程池带来如下影响。
1)当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,
因此线程池中的线程数不会超过 corePoolSize。
2)由于 1,使用无界队列时 maximumPoolSize 将是一个无效参数。
3)由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数。
4)更重要的,使用无界 queue 可能会耗尽系统资源,有界队列则有助于防止资源耗尽,同时即使使用有界队列,也要尽量控制队列的大小在一个合适的范围。
所以我们一般会使用,ArrayBlockingQueue、LinkedBlockingQueue、
SynchronousQueue、PriorityBlockingQueue。
threadFactory
创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具
有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有
的线程为守护线程。
Executors 静态工厂里默认的 threadFactory,线程的命名规则是“pool-数字-thread-数字”。
RejectedExecutionHandler
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了 4 种策略:
(1)AbortPolicy:直接抛出异常,默认策略;
(2)CallerRunsPolicy:用调用者所在的线程来执行任务;
(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
(4)DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
提交任务:
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
submit()方法用于提交需要返回值的任务。线程池会返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过 future的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
关闭线程池:
可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,
shutdownNow 首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown 只是将线程池的状态设置成 SHUTDOWN 状态,然后中断 所有没有正在执行任务的线程。只要调用了这两个关闭方法中的任意一个,isShutdown 方法就会返回 true。
当所有的任务都已关闭后,才表示线程池关闭成功,这时调用 isTerminaed 方法会返回 true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用 shutdown 方法来关闭线程池,如果任务不一定要执行完,则可以调用 shutdownNow 方法。
合理地配置线程池:
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。
•任务的性质:CPU 密集型任务、IO 密集型任务和混合型任务。
•任务的优先级:高、中和低。
•任务的执行时间:长、中和短。
•任务的依赖性:是否依赖其他系统资源,如数据库连接。
性质不同的任务可以用不同规模的线程池分开处理。
CPU 密集型任务应配置尽可能小的线程,如配置 Ncpu+1 个线程的线程池。
由于 IO 密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。
混合型的任务,如果可以拆分,将其拆分成一个 CPU 密集型任务和一个 IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU 个数。
预定义线程池
FixedThreadPool:
创建使用固定线程数的 FixedThreadPool 的 API。适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。
FixedThreadPool 的 corePoolSize 和 maximumPoolSize 都被设置为创建FixedThreadPool 时指定的参数 nThreads。当线程池中的线程数大于 corePoolSize 时,keepAliveTime 为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止。这里把 keepAliveTime 设置为 0L,意味着多余的空闲线程会被立即终止。FixedThreadPool 使用有界队列 LinkedBlockingQueue 作为线程池的工作队列
(队列的容量为 Integer.MAX_VALUE)。
SingleThreadExecutor:
创建使用单个线程的 SingleThread-Executor 的 API,于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。corePoolSize 和maximumPoolSize 被设置为 1。其他参数与 FixedThreadPool相同。SingleThreadExecutor 使用有界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为 Integer.MAX_VALUE)。
CachedThreadPool
创建一个会根据需要创建新线程的 CachedThreadPool 的 API。大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。corePoolSize 被设置为 0,即 corePool 为空;maximumPoolSize 被设置为Integer.MAX_VALUE。这里把 keepAliveTime 设置为 60L,意味着 CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。
FixedThreadPool 和 SingleThreadExecutor 使用有界队列 LinkedBlockingQueue作为线程池的工作队列。CachedThreadPool 使用没有容量的 SynchronousQueue作为线程池的工作队列,但 CachedThreadPool 的 maximumPool 是无界的。这意味着,如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新线程。极端情况下,CachedThreadPool 会因为创建过多线程而耗尽 CPU 和内存资源。
WorkStealingPool:
利用所有运行的处理器数目来创建一个工作窃取的线程池,使用 forkjoin 实现
CompletionService
CompletionService 实际上可以看做是 Executor 和 BlockingQueue 的结合体。
CompletionService 在接收到要执行的任务时,通过类似 BlockingQueue 的 put 和
take 获得任务执行的结果。
CompletionService 的一个实现是 ExecutorCompletionService,
ExecutorCompletionService 把具体的计算任务交给 Executor 完成。在实现上,ExecutorCompletionService 在构造函数中会创建一个BlockingQueue(使用的基于链表的 LinkedBlockingQueue),该 BlockingQueue 的作用是保存 Executor 执行的结果。当提交一个任务到 ExecutorCompletionService 时,首先将任务包装成QueueingFuture,它是 FutureTask 的一个子类,然后改写 FutureTask 的 done 方法,之后把 Executor 执行的计算结果放入 BlockingQueue 中。与 ExecutorService 最主要的区别在于 submit 的 task 不一定是按照加入时的顺序完成的。CompletionService 对 ExecutorService 进行了包装,内部维护一个保存 Future 对象的 BlockingQueue。只有当这个 Future 对象状态是结束的时候,才会加入到这个 Queue 中,take()方法其实就是 Producer-Consumer 中的Consumer。它会从 Queue 中取出 Future 对象,如果 Queue 是空的,就会阻塞在那里,直到有完成的 Future 对象加入到 Queue 中。所以,先完成的必定先被取出。这样就减少了不必要的等待时间。