1. 什么是线程池?线程池的工作原理?
2. 使用线程池的好处?
3. 线程池的注意事项。
4. 简单线程池的设计。
一个线程池管理了一组工作线程,同时它还包括了一个用于放置等待执行
任务的任务队列(阻塞队列)。
默认情况下,在创建了线程池后,线程池中的线程数为 0。当任务提交给
线程池之后的处理策略如下:
1. 如果此时线程池中的数量小于 corePoolSize(核心池的大小),即
使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务(也
就是每来一个任务,就要创建一个线程来执行任务)。
2. 如果此时线程池中的数量大于等于 corePoolSize,但是缓冲队列
workQueue 未满,那么任务被放入缓冲队列,则该任务会等待空闲线程将其
取出去执行。
3. 如果此时线程池中的数量大于等于 corePoolSize, 缓冲队列
workQueue 满,并且线程池中的数量小于 maximumPoolSize(线程池
最大线程数),建新的线程来处理被添加的任务。
4. 如果此时线程池中的数量大于等于 corePoolSize , 缓冲队列
workQueue 满,并且线程池中的数量等于 maximumPoolSize,那么通
过 RejectedExecutionHandler 所指定的策略(任务拒绝策略)来处理此任务。
也就是处理任务的优先级为: 核 心 线 程 corePoolSize 、 任 务 队 列
workQueue、最大线程 maximumPoolSize,如果三者都满了,使用
handler 处理被拒绝的任务。
5. 特别注意,在 corePoolSize 和 maximumPoolSize 之间的线程
数会被自动释放。当线程池中线程数量大于 corePoolSize 时,如果某线程
空闲时间超过 keepAliveTime,线程将被终止,直至线程池中的线程数目不
大于 corePoolSize。这样,线程池可以动态的调整池中的线程数。
1.通过重复利用已创建的线程,减少在创建和销毁线程上所花的时间以及系
统资源的开销。
2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就可以立即
执行。
3.提高线程的可管理性。使用线程池可以对线程进行统一的分配和监控。
4.如果不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内
存。
Executor 接口
Executors 类
ExecutorService 接口
AbstractExecutorService 抽象类
ThreadPoolExecutor 类
Executor 是一个顶层接口,在它里面只声明了一个方法 execute(Runnable),返回
值为 void,参数为 Runnable 类型,从字面意思可以理解,就是用来执行传进去的任务的;
然后 ExecutorService 接口继承了 Executor 接口,并声明了一些方法:submit、
invokeAll、invokeAny 以及 shutDown 等;
抽象类 AbstractExecutorService 实现了 ExecutorService 接口,基本实现了
ExecutorService 中声明的所有方法;
然后 ThreadPoolExecutor 继承了类 AbstractExecutorService。
public static ScheduledExecutorService newScheduledThreadPool(int
corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代 Timer 类。
public interface Executor {
void execute(Runnable command);
}
Executor 接口只有一个方法 execute(),并且需要传入一个 Runnable
类型的参数。那么它的作用自然是 具体的执行参数传入的任务。
public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
......
}它主要用来创建线程池。
Executors.newSingleThreadExecutor(); //创建容量为 1 的缓冲池 Executors.newFixedThreadPool(int n); //创建固定容量大小的缓冲池 Executors.newCachedThreadPool(); //创建一个缓冲池,缓冲池最大容量为Integer.MAX_VALUE(无界线程池)
下面是这三个静态方法的具体实现:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
1. newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串
行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。
此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2.newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的
最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结
束,那么线程池会补充一个新线程。
3. newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就
会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添
加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系
统(或者说 JVM)能够创建的最大线程大小。
在 ThreadPoolExecutor 类中提供了四个构造方法:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
不过在 java doc 中,并不提倡我们直接使用 ThreadPoolExecutor 创
建线程池,而是使用 Executors 类中提供的几个静态方法来创建线程池。
下面解释一下构造器中各个参数的含义:
corePoolSize:核心池的大小。默认情况下,在创建了线程池后,线程池中
的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中
的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中。
maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多
少个线程。
keepAliveTime:默认情况下,只有当线程池中的线程数大于 corePoolSize
时, keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize,
即当线程池中的线程数大于 corePoolSize 时,如果一个线程空闲的时间达到
keepAliveTime,则会终止,直到线程池中的线程数不超过 corePoolSize。
unit:参数 keepAliveTime 的时间单位。
workQueue:一个阻塞队列,任务缓存队列,即 workQueue,它用来存
放等待执行的任务。
workQueue 的类型为 BlockingQueue,通常可以取下面三种类型:
1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列
大小,则默认为 Integer.MAX_VALUE;
3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接
新建一个线程来执行新来的任务。在某次添加元素后必须等待其他线程取走后才能继续添
加。
ThreadFactory:线程工厂,主要用来创建线程。
handler:表示当拒绝处理任务时的策略,有以下四种取值:
1.AbortPolicy:直接抛出异常(默认的)
2.DiscardPolicy:直接丢弃任务
3.DiscardOldestPolicy:丢弃队列中最旧(队头)的任务,并执行当前
任务
4.CallerRunsPolicy:不用线程池中的线程执行,用调用者所在线程执
行。
在 ThreadPoolExecutor 类中有几个非常重要的方法:
execute()
submit()
shutdown()
shutdownNow()
submit 有返回值,execute 没有返回值。 所以说可以根据任务有无返回
值选择对应的方法。
*** submit 方便异常的处理。 如果任务可能会抛出异常,而且希望外面的调
用者能够感知这些异常,那么就需要调用 submit 方法,通过捕获 Future.get
抛出的异常。***
shutdown()和 shutdownNow()是用来关闭线程池的。
shutdown 方法:此方法执行后不得向线程池再提交任务,如果有空闲线
程则销毁空闲线程,等待所有正在执行的任务及位于阻塞队列中的任务执行结
束,然后销毁所有线程。
shutdownNow 方法:此方法执行后不得向线程池再提交任务,如果有空
闲线程则销毁空闲线程,取消所有位于阻塞队列中的任务,并将其放入
List容器,作为返回值。取消正在执行的线程(实际上仅仅是设
置正在执行线程的中断标志位,调用线程的 interrupt 方法来中断线程)。
虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。
(1)线程池的大小。多线程应用并非线程越多越好,需要根据系统运行的软硬件环境以及
应用本身的特点决定线程池的大小。一般来说,如果代码结构合理的话,线程数目与 CPU
数量相适合即可。如果线程运行时可能出现阻塞现象,可相应增加池的大小;如有必要可
采用自适应算法来动态调整线程池的大小,以提高 CPU 的有效利用率和系统的整体性能。
(2)并发错误。多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意避
免死锁现象的发生。
(3)线程泄漏。这是线程池应用中一个严重的问题,当任务执行完毕而线程没能返回池中
就会发生线程泄漏现象。
一个典型的线程池,应该包括如下几个部分:
1、线程池管理器(ThreadPool),用于启动、停用,管理线程池
2、工作线程(WorkThread),线程池中的线程
3、请求接口(WorkRequest),创建请求对象,以供工作线程调度任务的执行
4、请求队列(RequestQueue),用于存放和提取请求
5、结果队列(ResultQueue),用于存储请求执行后返回的结果
线程池管理器,通过添加请求的方法(putRequest)向请求队列(RequestQueue)
添加请求,这些请求事先需要实现请求接口,即传递工作函数、参数、结果处理函数、以
及异常处理函数。之后初始化一定数量的工作线程,这些线程通过轮询的方式不断查看请
求队列(RequestQueue),只要有请求存在,则会提取出请求,进行执行。然后,线程池
管理器调用方法(poll)查看结果队列(resultQueue)是否有值,如果有值,则取出,调
用结果处理函数执行。通过以上讲述,不难发现,这个系统的核心资源在于请求队列和结
果队列,工作线程通过轮询 requestQueue 获得任务,主线程通过查看结果队列,获得执
行结果。因此,对这个队列的设计,要实现线程同步,以及一定阻塞和超时机制的设计,
以防止因为不断轮询而导致的过多 cpu 开销。
如何合理的配置 java 线程池?如 CPU 密集型的任务,基本线程池
应该配置多大?IO 密集型的任务,基本线程池应该配置多大?用有界
队列好还是无界队列好?任务非常多的时候,使用什么阻塞队列能获取
最好的吞吐量?
1)配置线程池时,CPU 密集型任务可以少配置线程数,大概和机器的 cpu 核数相
当,可以使得每个线程都在执行任务。
2)IO 密集型任务则由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽
可能多的线程,2*cpu 核数。
3)有界队列和无界队列的配置需区分业务场景,一般情况下配置有界队列,在一些
可能会有爆发性增长的情况下使用无界队列。
4)任务非常多时,使用非阻塞队列使用 CAS 操作替代锁可以获得好的吞吐量。
synchronousQueue 吞吐率最高。
