主要用于概念扫盲和梳理。
本文主要为什么使用线程池,ThreadPoolExecutor构造函数,常见的线程池种类。
一. 为什么用线程池
1. 系统开销
创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率
例如:
记创建线程消耗时间T1,执行任务消耗时间T2,销毁线程消耗时间T3
如果T1+T3>T2
,那么是不是说开启一个线程来执行这个任务太不划算了!
线程池缓存线程,可复用的闲置线程来执行新任务,避免了T1+T3带来的系统开销
2. 系统资源
线程并发数量过多,抢占系统资源从而导致阻塞
我们知道线程能共享系统资源,如果同时执行的线程过多,就有可能导致系统资源不足而产生阻塞的情况
运用线程池能有效的控制线程最大并发数,避免以上的问题
3. 管理线程
对线程进行一些简单的管理
比如:延时执行、定时循环执行的策略等
运用线程池都能进行很好的实现>
二. 继承关系
总览
常用的几个类或接口
ExecutorService
线程池接口,继承于Executor接口,多了Submit()
、shutdown()
等方法
ScheduledExecutorService
功能和Timer类似,解决那些需要任务重复执行的问题,负责线程的调度
ThreadPoolExecutor
线程池的实现类
ScheduledThreadPoolExecutor
继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。
Executors
一个工具类
,提供几个创建线程池的方法
Future 接口
它和FutureTask类(实现了Future接口)都用来表示异步计算的结果。
三. 构造线程池
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类。
- 在Java中,线程池的概念是Executor这个接口,具体实现为ThreadPoolExecutor类
- 对线程池的配置,就是对ThreadPoolExecutor构造函数的参数的配置
构造函数
七个参数 :心 总 时 纲 队 厂 异
五个参数的构造函数
1 | public ThreadPoolExecutor(int corePoolSize, |
六个参数的构造函数-1
1 | public ThreadPoolExecutor(int corePoolSize, |
六个参数的构造函数-2
1 | public ThreadPoolExecutor(int corePoolSize, |
七个参数的构造函数
1 | public ThreadPoolExecutor(int corePoolSize, |
参数解释
1. int corePoolSize —核心线程数最大值
- 线程池新建线程的时候
- 如果当前线程总数小于corePoolSize,则新建的是核心线程
- 如果超过corePoolSize,则新建的是非核心线程
- 核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。
- 如果指定ThreadPoolExecutor的
allowCoreThreadTimeOut
这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉
2. int maximumPoolSize—线程总数最大值
- 线程总数 = 核心线程数 + 非核心线程数
3. long keepAliveTime—非核心线程闲置超时时长
- 一个
非核心线程
,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉 - 如果设置allowCoreThreadTimeOut = true,则会作用于核心线程
4. TimeUnit unit—量纲
keepAliveTime的单位,TimeUnit是一个枚举类型,其包括:
单位 | 时长 |
---|---|
NANOSECONDS | 1微毫秒 = 1微秒 / 1000 |
MICROSECONDS | 1微秒 = 1毫秒 / 1000 |
MILLISECONDS | 1毫秒 = 1秒 /1000 |
SECONDS | 秒 |
MINUTES | 分 |
HOURS | 小时 |
DAYS | 天 |
5. BlockingQueue workQueue–任务队列
- 维护着等待执行的Runnable对象
- 当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务
常用的workQueue类型:
SynchronousQueue
它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现线程数达到了maximumPoolSize而不能新建线程
的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大
LinkedBlockingQueue
基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE
这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。
由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize
ArrayBlockingQueue
基于数组的先进先出队列,此队列创建时必须指定大小
可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误
DelayQueue
队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务
6. ThreadFactory threadFactory
主要用来创建线程,是一个接口,new他的时候需要实现他的Thread newThread(Runnable r)方法,一般用不上
AsyncTask新建线程池的threadFactory参数源码:
1 | new ThreadFactory() { |
7. RejectedExecutionHandler handler
表示当拒绝处理任务时的策略,有以下四种取值:
取值 | 策略 |
---|---|
ThreadPoolExecutor.AbortPolicy(默认) | 丢弃 任务并抛出 RejectedExecutionException异常 |
ThreadPoolExecutor.DiscardPolicy | 也是丢弃 任务,但是不抛出 异常 |
ThreadPoolExecutor.DiscardOldestPolicy | 丢弃队列最前面 的任务,然后重新尝试执行任务(重复此过程) |
ThreadPoolExecutor.CallerRunsPolicy | 由调用线程处理该任务 ,线程调用运行该任务的execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度 |
ThreadPoolExecutor的策略
上面介绍参数的时候其实已经说到了ThreadPoolExecutor执行的策略,这里给总结一下,当一个任务被添加进线程池时:
- 核心线程满了吗
1.1 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
1.2 线程数量达到了corePools,则将任务移入队列等待 - 队列满了吗
2.1 队列已满,新建线程(非核心线程)执行任务
2.2 队列已满,总线程数又达到了maximumPoolSize,就会由RejectedExecutionHandler
抛出异常
总结起来,也即是说,当有新的任务要处理时,先看线程池中的线程数量是否大于 corePoolSize,再看缓冲队列 workQueue 是否满,最后看线程池中的线程数量是否大于 maximumPoolSize。
另外,当线程池中的线程数量大于 corePoolSize 时,如果里面有线程的空闲时间超过了 keepAliveTime,就将其移除线程池,这样,可以动态地调整线程池中线程的数量。
ThreadPoolExecutor类中重要方法
方法 | 作用 |
---|---|
execute() | 通过这个方法可以向线程池提交一个任务,交由线程池去执行 |
submit() | 实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果 |
shutdown() | 不会立即终止线程池,而是要等 所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务 |
shutdownNow() | 立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务 |
四. Executor与Executors
关系
Java里面线程池的顶级接口是 Executor,不过真正的线程池接口是 ExecutorService, ExecutorService 的默认实现是 ThreadPoolExecutor;普通类 Executors 里面调用的就是 ThreadPoolExecutor。
对比
Executor 框架
Executor框架在Java 5中被引入,Executor 框架是一个根据一组执行策略调用、调度、执行和控制的异步任务的框架
无限制的创建线程会引起应用程序内存溢出,所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executor 框架可以非常方便的创建一个线程池。Executors 类
Executors为Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 类提供了一些工具方法。Executors 可以用于方便的创建线程池。
五. 常见四种线程池
Java通过Executors提供了四种线程池,这四种线程池都是直接或间接配置ThreadPoolExecutor的参数实现的
1. CachedThreadPool()–可缓存线程池
特点
- 线程数无限制
- 有空闲线程则复用空闲线程,若无空闲线程则新建线程
- 一定程度上减少频繁创建/销毁线程,减少系统开销
场合
缓存型池子通常用于执行一些生存期很短的异步型任务 因此在一些面向连接的 daemon 型 SERVER 中用得不多。
但对于生存期短的异步任务,它是 Executor 的首选。
源码:
1 | public static ExecutorService newCachedThreadPool() { |
2. FixedThreadPool(int nThreads)–定长线程池
特点
- 可控制线程最大并发数(同时执行的线程数)
- 超出的线程会在队列中等待
场合
FixedThreadPool 多数针对一些很稳定很固定的正规并发线程,多用于服务器。
//threadFactory => 创建线程的方法,这就是我叫你别理他的那个星期六!你还看!
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory);
源码:
1 | public static ExecutorService newFixedThreadPool(int nThreads) { |
2个参数的构造方法源码,不用我贴你也知道他把星期六放在了哪个位置!所以我就不贴了,省下篇幅给我扯皮
3. ScheduledThreadPool(int corePoolSize)–定核心线程池
- 支持定时及周期性任务执行。
创建方法:
//nThreads => 最大线程数即maximumPoolSize
ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);
源码:
1 | public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { |
4. SingleThreadExecutor()–单线程化的线程池
- 有且仅有一个工作线程执行任务
- 所有任务按照指定顺序执行,即遵循队列的入队出队规则
创建方法:
ExecutorService singleThreadPool = Executors.newSingleThreadPool();
源码:
1 | public static ExecutorService newSingleThreadExecutor() { |
六. 实际例子
Executor 执行 Runnable 任务
通过 Executors 的以上四个静态工厂方法获得 ExecutorService 实例,而后调用该实例的 execute(Runnable command)方法即可。一旦 Runnable 任务传递到 execute()方法,该方法便会自动在一个线程上执行。
1 | import java.util.concurrent.ExecutorService; |
输出:(每次可能不一样)
1 | ************* a0 ************* |
Executor 执行 Callable 任务
在 Java 5 之后,任务分两类:一类是实现了 Runnable 接口的类,一类是实现了 Callable 接口的类。两者都可以被 ExecutorService 执行,但是 Runnable 任务没有返回值,而 Callable 任务有返回值。并且 Callable 的 call()方法只能通过 ExecutorService 的 submit(Callable task)
方法来执行,并且返回一个 Future,是表示任务等待完成的 Future。
1 | import java.util.ArrayList; |
输出:(每次可能不一样)
1 | call()方法被自动调用!!! pool-1-thread-1 |
参考文章
线程池,这一篇或许就够了
Java并发编程:线程池的使用
官方文档
【Java多线程】线程池的工作原理详解(上)
极客学院