问题:
1.单机上一个线程正在处理服务,如果忽然断电了怎么办(正在处理和阻塞队列里的请求怎么处理)
2.为什么要使用线程池,线程池用什么用
3.说说几种常见的线程池及使用场景
4.线程池有哪几种工作队列
5.怎么理解无界队列和有界队列
6.线程池中的几种重要的参数及流程
什么是线程池
线程池的概念大家应该都很清楚,帮我们重复管理线程,避免创建大量的线程增加开销。
除了降低开销以外,线程池也可以提高响应速度,了解点 JVM 的同学可能知道,一个对象的创建大概需要经过以下几步:
- 检查对应的类是否已经被加载、解析和初始化
- 类加载后,为新生对象分配内存
- 将分配到的内存空间初始为 0
- 对对象进行关键信息的设置,比如对象的哈希码等
- 然后执行 init 方法初始化对象
创建一个对象的开销需要经过这么多步,也是需要时间的嘛,那可以复用已经创建好的线程的线程池,自然也在提高响应速度上做了贡献。
java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池
用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
线程池的应用场景
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:
假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。
线程池的组成
一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的 收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
ThreadPoolExecutor类中其他的一些比较重要成员变量:
private final BlockingQueue<Runnable> workQueue; //任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock = new ReentrantLock(); //线程池的主要状态锁,对线程池状态(比如线程池大小
//、runState等)的改变都要使用这个锁
private final HashSet<Worker> workers = new HashSet<Worker>(); //用来存放工作集
private volatile long keepAliveTime; //线程存活时间
private volatile boolean allowCoreThreadTimeOut; //是否允许为核心线程设置存活时间
private volatile int corePoolSize; //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int maximumPoolSize; //线程池最大能容忍的线程数
private volatile int poolSize; //线程池中当前的线程数
private volatile RejectedExecutionHandler handler; //任务拒绝策略
private volatile ThreadFactory threadFactory; //线程工厂,用来创建线程
private int largestPoolSize; //用来记录线程池中曾经出现过的最大线程数
private long completedTaskCount; //用来记录已经执行完毕的任务个数
线程池的“真相”
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
submit
方法:
submit
方法会提交一个任务去给线程池执行,该任务可以是带返回结果的 Callable<V>
任务,也可以是一开始就指定结果的 Runnable
任务,或者不带结果的 Runnable
任务(此时即一开始指定结果为 null
)。submit
方法会返回一个与所提交任务相关联的 Future<V>
。通过 上一篇文章 我们可以知道,Future<V>
的 get
方法可以等待任务执行完毕并返回结果。所以通过 Future<V>
,我们可以与已经提交到线程池的任务进行交互。submit
提交任务及任务运行过程大致如下:
- 向线程池提交一个
Runnable
或者Callable<V>
任务; - 将 任务 作为参数使用
newTaskFor
方法构造出FutureTask<V>
;通过Future<V>
的get
方法,获得任务的结果。
invokeAll
方法:
invokeAll
方法可以一次执行多个任务,但它并不同等于多次调用 submit
方法。submit
方法是非阻塞的,每次调用 submit
方法提交任务到线程池之后,会立即返回与任务相关联的 Future<V>
,然后当前线程继续向后执行;
而 invokeAll
方法是阻塞的,只有当提交的多个任务都执行完毕之后,invokeAll
方法才会返回,执行结果会以List<Future<V>>
返回,该 List<Future<V>>
中的每个 Future<V>
是和提交任务时的 Collection<Callable<V>>
中的任务 Callable<V>
一 一对应的。带 timeout 参数的 invokeAll
就是设置一个超时时间,如果超过这个时间 invokeAll
中提交的所有任务还有没全部执行完,那么没有执行完的任务会被取消(中断),之后同样以一个 List<Future<V>>
返回执行的结果。
invokeAny
方法:
invokeAny
方法也是阻塞的,与 invokeAll
方法的不同之处在于,当所提交的一组任务中的任何一个任务完成之后,invokeAny
方法便会返回(返回的结果便是那个已经完成的任务的返回值),而其他任务会被取消(中断)。
举一个 invokeAny
使用的例子:电脑有 C、D、E、F 四个盘,我们需要找一个文件,但是我们不知道这个文件位于哪个盘中,我们便可以使用 invokeAny
,并提交四个任务(对应于四个线程)分别查找 C、D、E、F 四个盘,如果哪个线程找到了这个文件,那么此时 invokeAny
便停止阻塞并返回结果,同时取消其他任务。
shutdown
方法:
shutdown
方法的作用是向线程池发送关闭的指令。一旦在线程池上调用 shutdown
方法之后,线程池便不能再接受新的任务;如果此时还向线程池提交任务,那么将会抛出 RejectedExecutionException
异常。之后线程池不会立刻关闭,直到之前已经提交到线程池中的所有任务(包括正在运行的任务和在队列中等待的任务)都已经处理完成,才会关闭。
shutdownNow
方法:
</