版权声明:本文为博主原创文章,未经博主允许不得转载
源码:github.com/AnliaLee
大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论
本篇博客我们将开始探索由上一章引出的线程池(ThreadPoolExecutor)的知识。由于内含大量示例,导致文章篇幅有点长,望大家耐心食用…
往期回顾
大话Android多线程(一) Thread和Runnable的联系和区别
大话Android多线程(二) synchronized使用解析
大话Android多线程(三) 线程间的通信机制之Handler
大话Android多线程(四) Callable、Future和FutureTask
简介这东西也写不出啥花样来,遂直接偷懒引用别人的吧哈哈
new Thread()的缺点
• 每次new Thread()耗费性能
• 调用new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制创建,之间相互竞争,会导致过多占用系统资源导致系统瘫痪
• 不利于扩展,比如如定时执行、定期执行、线程中断
采用线程池的优点
• 重用存在的线程,减少对象创建、消亡的开销,性能佳
• 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞
• 提供定时执行、定期执行、单线程、并发数控制等功能
以上内容摘自Android线程管理之ExecutorService线程池
线程池ThreadPoolExecutor的继承关系如下图所示
下一节我们将介绍ThreadPoolExecutor的构造参数
构造ThreadPoolExecutor时需传入许多参数,我们以参数最多的那个构造方法为例(因为参数threadFactory和handler是有默认值的,所以和其他几个构造方法的区别只是有无设置这两个参数的入口而已,就不赘述了)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
我们将参数分析代入到程序员开发的故事中,让大家更容易理解,以下是参数介绍
int corePoolSize:
计划招聘核心程序员的数量。核心程序员是公司的顶梁柱,公司接到甲方需求(即任务)后会优先分配给核心程序员去开发
线程池中核心线程的数量
int maximumPoolSize:
计划招聘程序员的总数。程序员由核心程序员和实习生组成
线程池中线程数的最大值
long keepAliveTime:
允许员工打酱油的时间。公司招了实习生之后,如果发现一段时间(keepAliveTime)内实习生没活干,在那偷懒刷什么掘金沸点的时候,就会把他辞掉。当然核心程序员抱着的也不一定是铁饭碗,若公司采取了节省成本的经营策略(ThreadPoolExecutor.allowCoreThreadTimeOut设为true),核心程序员一段时间没活干也一样会被裁员
线程的闲置时长,默认情况下此参数只作用于非核心线程,即非核心线程闲置时间超过keepAliveTime后就会被回收。但如果ThreadPoolExecutor.allowCoreThreadTimeOut设为true,则参数同样可以作用于核心线程
TimeUnit unit:
上面时间参数的单位,有纳秒、微秒、毫秒、秒、分、时、天
可供选择的单位类型有:
TimeUnit.NANOSECONDS:纳秒
TimeUnit.MICROSECONDS:微秒
TimeUnit.MILLISECONDS:毫秒
TimeUnit.SECONDS:秒
TimeUnit.MINUTES:分
TimeUnit.HOURS:小时
TimeUnit.DAYS:天
BlockingQueue< Runnable> workQueue:
储备任务的队列
线程池中的任务队列,该队列主要用来存储已经提交但尚未分配给线程执行的任务。BlockingQueue,即阻塞队列,可供传入的队列类型有:
• ArrayBlockingQueue:基于数组的阻塞队列
• LinkedBlockingQueue:基于链表的阻塞队列
• PriorityBlockingQueue:基于优先级的阻塞队列
• DelayQueue:基于延迟时间优先级的阻塞队列
• SynchronousQueue:基于同步的阻塞队列
我们在下面的章节中将会详细对比以上这几种队列的区别。此外,还需注意传入任务的都需实现Runnable接口
ThreadFactory threadFactory:
线程工厂接口,只有一个new Thread(Runnable r)方法,可以为线程池创建新线程。系统为我们提供了默认的threadFactory:Executors.defaultThreadFactory(),我们一般使用默认的就可以了
RejectedExecutionHandler handler:
拒绝策略,默认使用ThreadPoolExecutor.AbortPolicy,当新任务被拒绝时会将抛出RejectExecutorException异常。此外还有3种策略可供选择:CallerRunsPolicy,DiscardPolicy和DiscardOldestPolicy
当我们使用submit或者execute方法将任务提交到线程池时,线程池遵循以下策略将任务分配给相应线程去执行(任务队列使用最基本的ArrayBlockingQueue)
HR根据任务执行情况来决定何时招核心程序员,如果接到一个需求后发现核心程序员手上都有任务(或者一个程序员都没有的时候),就会招一个进来,招满为止
提交任务后,如果线程池中的线程数未达到核心线程的数量(corePoolSize),则会创建一个核心线程去执行
举个栗子,我们设置任务数(taskSize)为3,核心线程数(corePoolSize)为5,则 taskSize < corePoolSize,线程池会创建3条核心线程去执行任务(假设每个任务都需要一定时间才能完成)
public class ExecutorTest {
//省略部分代码...
private static int taskSize = 3;//任务数
private static int corePoolSize = 5;//核心线程的数量
private static int maximumPoolSize = 20;//线程数的最大值
private static int queueSize = 128;//可储存的任务数
public static class TestTask implements Runnable {
public void run() {
if (taskSize > 0) {
try{
Thread.sleep(500);//模拟开发时间
System.out.println(getTime() + getName(Thread.currentThread().getName())
+ " 完成一个开发任务,编号为t" + (taskSize--)
);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
public static void main(String args[]){
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
1,
TimeUnit.SECONDS,
new ArrayBlockingQueue(queueSize)
);
TestTask task;
int size = taskSize;
for (int i = 0; i < size; i++) {
task = new TestTask();
executor.execute(task);
}
executor.shutdown();
}
}
运行结果见下图(请忽略任务编号,现在还没用到)
又有新的任务下来时,如果核心程序员有人空闲,就扔给他做;如果手上都有任务,则暂时保存到任务储备清单(workQueue)中,等到谁有空了再交给他做。当然这个队列可存储的任务数量是有限制的
提交任务后,如果线程池中的线程数已经达到核心线程的数量(corePoolSize),但任务队列(workQueue)中存储的任务数未达到最大值,则将任务存入任务队列中等待执行
我们将任务数设为10,核心线程数设为3,任务队列的最大值设为7,此时将任务分配给核心线程后刚好可以填满任务队列
private static int taskSize = 10;//任务数
private static int corePoolSize = 3;//核心线程的数量
private static int maximumPoolSize = 10;//线程数的最大值
private static int queueSize = 7;//可储存的任务数
运行结果见下图
当核心组程序员手上的任务和储备的任务(workQueue)都达到饱和时,会招聘一个实习生(非核心线程)来分担任务
提交任务后,如果线程池中的线程数达到核心线程数但未超过线程数的最大值,同时任务队列中的任务数已达到最大值,则创建一个非核心线程来执行任务
我们将之前的任务数改为12,其他数值不变,那么将会有两位实习生参与到开发中(taskSize - (corePoolSize + queueSize) = 2)
private static int taskSize = 12;//任务数
private static int corePoolSize = 3;//核心线程的数量
private static int maximumPoolSize = 10;//线程数的最大值
private static int queueSize = 7;//可储存的任务数
运行结果见下图( 多了实习生4和5)
加…加班???
正确答案应该是推掉!拒绝!
提交任务后,若线程池中的线程数已达到最大值,且所有线程均在执行任务,任务队列也饱和了,则拒绝执行该任务,并根据拒绝策略执行相应操作
上个例子中一共创建了5条线程(3核心线程、2非核心线程),那么这次我们只将线程数的最大值改为4,采用默认的拒绝策略
private static int taskSize = 12;//任务数
private static int corePoolSize = 3;//核心线程的数量
private static int maximumPoolSize = 4;//线程数的最大值
private static int queueSize = 7;//可储存的任务数
public static void main(String args[]){
//省略部分代码...
for (int i = 0; i < size; i++) {
executor.execute(task);
System.out.println("接到任务 " + i);
}
executor.shutdown();
}
运行结果见下图,可以看见线程池只接收了11个任务(maximumPoolSize + queueSize = 11 ),在提交第12个任务后会抛出RejectedExecutionException的异常
另外需要注意的是,如果我们在提交任务时抛出了异常,那么之后调用的shutdown()将变为无效代码,线程池将一直运行在主线程中无法关闭
还有一种特殊情况,如果公司不打算招核心程序员,接到的任务又比任务队列的容量要少,这时公司为了节省开支就只会招一个实习生来完成开发任务
在 corePoolSize = 0 的条件下,提交任务后,若任务队列中的任务数仍未达到最大值,线程池只会创建一条非核心线程来执行任务
private static int taskSize = 9;//任务数
private static int corePoolSize = 0;//核心线程的数量
private static int maximumPoolSize = 5;//线程数的最大值
private static int queueSize = 10;//可储存的任务数
运行结果见下图
BlockingQueue是一个接口,它提供了3个添加元素方法:
3个删除元素的方法:
我们之前讲到的5种类型的队列实际上都是BlockingQueue的实现类,本篇博客不会具体分析源码的实现,我们只对比它们使用上的区别:
ArrayBlockingQueue(int capacity)
ArrayBlockingQueue(int capacity, boolean fair)
ArrayBlockingQueue(int capacity, boolean fair, Collection extends E> c)
基于链表(单向链表)的阻塞队列,和ArrayBlockingQueue类似,其内部维护着一个由单向链表构成的数据缓冲队列。区别于ArrayBlockingQueue,LinkedBlockingQueue在初始化的时候可以不用设置容量大小,其默认大小为Integer.MAX_VALUE(即2的31次方-1,表示 int 类型能够表示的最大值)。若设置了大小,则使用起来和ArrayBlockingQueue一样。LinkedBlockingQueue的构造方法如下
LinkedBlockingQueue()
LinkedBlockingQueue(int capacity)
LinkedBlockingQueue(Collection extends E> c)
这里参数和之前讲的一样,而且使用方法和ArrayBlockingQueue大同小异,就不赘述了
基于优先级的阻塞队列,用法类似于LinkedBlockingQueue,区别在于其存储的元素不是按照FIFO排序的,这些元素的排序规则得由我们自己来定义:所有插入PriorityBlockingQueue的对象元素必须实现Comparable(自然排序)接口,我们对Comparable接口的实现定义了队列优先级的排序规则
PriorityBlockingQueue的队列容量是“无界”的,因为新任务进来时如果发现已经超过了队列的初始容量,则会执行扩容的操作。这意味着如果corePoolSize > 0,线程池中的线程数达到核心线程数的最大值,且任务队列中的任务数也达到最大值,这时新的任务提交进来,线程池并不会创建非核心线程来执行新任务,而是对任务队列进行扩容
更加具体的内容大家可以去研究一下这篇篇博客:并发队列 – 无界阻塞优先级队列 PriorityBlockingQueue 原理探究
PriorityBlockingQueue的构造方法如下
PriorityBlockingQueue()
PriorityBlockingQueue(int initialCapacity)
PriorityBlockingQueue(int initialCapacity, Comparator super E> comparator)
PriorityBlockingQueue(Collection extends E> c)
重复的参数就不解释了
public class ExecutorTest {
//省略部分代码...
private static int taskSize = 9;//任务数
private static int corePoolSize = 0;//核心线程的数量
private static int maximumPoolSize = 5;//线程数的最大值
private static int queueSize = 10;//可储存的任务数
public static class PriorityTask implements Runnable,Comparable<PriorityTask>{
private int priority;
public PriorityTask(int priority) {
this.priority = priority;
}
@Override
public void run() {
if (taskSize > 0) {
try{
Thread.sleep(1000);//模拟开发时间
System.out.println(getTime() + getName(Thread.currentThread().getName())
+ " 完成一个开发任务,编号为t" + (taskSize--) + ", 优先级为:" + priority
);
}catch (Exception e){
e.printStackTrace();
}
}
}
@Override
public int compareTo(PriorityTask task) {
if(this.priority == task.priority){
return 0;
}
return this.priority1:-1;//优先级大的先执行
}
}
public static void main(String args[]){
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
1,
TimeUnit.SECONDS,
new PriorityBlockingQueue(queueSize)
);
Random random = new Random();
PriorityTask task;
int size = taskSize;
for (int i = 0; i < size; i++) {
int p = random.nextInt(100);
task = new PriorityTask(p);
executor.execute(task);
System.out.println("接到任务 " + i + ",优先级为:" + p);
}
executor.shutdown();
}
}
运行结果见下图
基于延迟时间优先级的阻塞队列,DelayQueue和PriorityBlockingQueue非常相似,同样是“无界”队列(DelayQueue不需要设置初始容量大小),同样基于优先级进行排序。但有一点不同,DelayQueue中的元素必须实现 Delayed接口(Delayed继承自Comparable接口),我们需重写Delayed.getDelay方法为元素的释放(执行任务)设置延迟(getDelay方法的返回值是队列元素被释放前的保持时间,如果返回0或一个负值,就意味着该元素已经到期需要被释放,因此我们一般用完成时间和当前系统时间作比较)。DelayQueue的构造方法如下
DelayQueue()
DelayQueue(Collection extends E> c)
这次我们将延迟时间当成是任务开发时间,设置开发时间越短的任务优先级越高
public class ExecutorTest {
//省略部分代码...
private static int taskSize = 5;//任务数
private static int corePoolSize = 0;//核心线程的数量
private static int maximumPoolSize = 5;//线程数的最大值
public static class DelayTask implements Runnable,Delayed{
private long finishTime;
private long delay;
public DelayTask(long delay){
this. delay= delay;
finishTime = (delay + System.currentTimeMillis());//计算出完成时间
}
@Override
public void run() {
if (taskSize > 0) {
try{
System.out.println(getTime() + getName(Thread.currentThread().getName())
+ " 完成一个开发任务,编号为t" + (taskSize--) + ", 用时:" + delay/1000
);
}catch (Exception e){
e.printStackTrace();
}
}
}
@Override
public long getDelay(@NonNull TimeUnit unit) {
//将完成时间和当前时间作比较,<=0 时说明元素到期需被释放
return (finishTime - System.currentTimeMillis());
}
@Override
public int compareTo(@NonNull Delayed o) {
DelayTask temp = (DelayTask) o;
return temp.delay < this.delay?1:-1;//延迟时间越短优先级越高
}
}
public static void main(String args[]){
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
1,
TimeUnit.SECONDS,
new DelayQueue()
);
Random random = new Random();
DelayTask task;
int size = taskSize;
for (int i = 0; i < size; i++) {
long d = 1000 + random.nextInt(10000);
task = new DelayTask(d);
executor.execute(task);
System.out.println("接到任务 " + i + ",预计完成时间为:" + d/1000);
}
executor.shutdown();
}
}
运行结果如下
基于同步的阻塞队列,这是一个非常特殊的队列,因为它内部并没有数据缓存空间,元素只有在试图取走的时候才有可能存在。也就是说,如果在插入元素时后续没有执行取出的操作,那么插入的行为就会被阻塞,如果SynchronousQueue是在线程池中使用的,那么这种场景下就会抛出RejectedExecutionException异常。可能这么解释有点绕,下面我们会通过讲解示例辅助大家理解,先来看构造方法
SynchronousQueue()
SynchronousQueue(boolean fair)
同样的,参数和之前一样,就不解释了,我们来看示例:
采用了SynchronousQueue的策略后,任务队列不能储存任务了。这意味着如果接到新任务时发现没人有空来开发(程序员手上都有任务,公司招人名额也满了),那这个新任务就泡汤了(抛出异常)
例如我们将核心程序员的数量(corePoolSize)设为3,程序员总数(maximumPoolSize)设为9,而任务数(taskSize)设为10
public class ExecutorTest {
//省略部分代码...
private static int taskSize = 10;//任务数
private static int corePoolSize = 3;//核心线程的数量
private static int maximumPoolSize = 9;//线程数的最大值
public static void main(String args[]){
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
1,
TimeUnit.SECONDS,
new SynchronousQueue()
);
TestTask task;
int size = taskSize;
for (int i = 0; i < size; i++) {
task = new TestTask();
executor.execute(task);
System.out.println("接到任务 " + i);
}
executor.shutdown();
}
}
在招满人的情况下,公司最多就9个程序员,当接到第10个任务时,发现没人可用了,就会抛出异常。当然,之前成功接收的任务不会受到影响
因此根据SynchronousQueue的特性,在使用SynchronousQueue时通常会将maximumPoolSize设为“无边界”,即Integer.MAX_VALUE(在系统为我们预设的线程池中,CachedThreadPool就是这么设置的,具体的我们后面再细说)
前面讲了这么多,其实都是教大家如何自定义一个线程池。系统为了方便我们进行开发,早已封装好了各种线程池供我们使用。我们可以用Executors.newXXX的方式去实例化我们需要的线程池。可供选择的线程池种类很多:
我们挑其中常用的4种讲讲就行(其实各种线程池的区别只是构建线程池时传入的参数不同而已,经过之前我们对任务执行策略和各种任务队列的讲解后,理解不同种类的线程池就变得非常简单了。这也正是博主要花费那么长的篇幅给大家举例子的原因,希望大家都能看得懂吧~)
我们直接看系统是如何封装的
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0,
Integer.MAX_VALUE,
60L,
TimeUnit.SECONDS,
new SynchronousQueue()
);
}
核心线程数为0,线程总数设为Integer.MAX_VALUE,线程的闲置时长为60s,任务队列为SynchronousQueue(同步队列),结合我们之前说的,可以总结出CachedThreadPool的特点如下:
使用示例如下,我们设置任务数为10
ExecutorService service = Executors.newCachedThreadPool();
TestTask task;
int size = taskSize;
for (int i = 0; i < size; i++) {
task = new TestTask();
service.execute(task);
}
service.shutdown();
源码如下
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads,
nThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()
);
}
核心线程数由我们自定义,最大线程数和核心线程数相等,线程闲置时间为0,任务队列为LinkedBlockingQueue,所以FixedThreadPool的特点如下:
使用示例如下,我们设置任务数为10,核心线程数为5
ExecutorService service = Executors.newFixedThreadPool(corePoolSize);
TestTask task;
int size = taskSize;
for (int i = 0; i < size; i++) {
task = new TestTask();
service.execute(task);
}
service.shutdown();
源码如下
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1,
1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
核心线程数为1,最大线程数也是1,线程闲置时间为0,任务队列为LinkedBlockingQueue,且SingleThreadExecutor是FinalizableDelegatedExecutorService类的实例,所以SingleThreadExecutor的特点如下:
* 只有一个核心线程,所有任务都在同一线程中按顺序完成
* 任务队列容量没有大小限制
* 如果单个线程在执行过程中因为某些错误而中止,会创建新的线程会替代它执行后续的任务(区别于 newFixedThreadPool(1) ,如果线程遇到错误中止,newFixedThreadPool(1) 是无法创建替代线程的)
* 使用SingleThreadExecutor我们就不需要处理线程同步的问题了
使用示例如下,我们设置任务数为10
ExecutorService service = Executors.newSingleThreadExecutor();
TestTask task;
int size = taskSize;
for (int i = 0; i < size; i++) {
task = new TestTask();
service.execute(task);
}
service.shutdown();
源码如下
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(
corePoolSize,
Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, //DEFAULT_KEEPALIVE_MILLIS = 10L
MILLISECONDS,
new DelayedWorkQueue()
);
}
核心线程数由我们自定义,最大线程数为Integer.MAX_VALUE,线程闲置时长为10毫秒,任务队列采用了DelayedWorkQueue(和DelayQueue非常像)。ScheduledThreadPool的特点如下:
使用示例如下,我们调用ScheduledExecutorService.schedule方法提交延迟启动的任务,延迟时间为3秒:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
Runnable runnable = new Runnable(){
@Override
public void run() {
System.out.println("开始执行任务,时间:" + getTime());
}
};
scheduledExecutorService.schedule(runnable,3,TimeUnit.SECONDS);
System.out.println("提交任务,时间:" + getTime());
此外还有scheduleAtFixedRate,scheduleWithFixedDelay等提交任务的方法,就不一一举例了
1、我们除了用execute方法提交任务以外,还可以使用submit方法。submit方法提交的任务需实现Callable接口(有关Callable的知识可以看下我上一篇博客:大话Android多线程(四) Callable、Future和FutureTask),因此其具有返回值
2、线程池有两种手动关闭的方法:
3、如何合理地估算线程池大小?
emmmm…基本就这些内容了,博主已经尽可能地覆盖线程池的所有知识了(除了源码解析,以后有机会会出一个单章分析下源码),若有什么遗漏或者建议的欢迎留言评论。如果觉得博主写得还不错麻烦点个赞,你们的支持是我最大的动力~