1.使用线程池的好处(为什么使用线程池)
1.减少资源消耗
Thread线程,是操作系统的资源,创建和销毁是要有资源消耗的,如果有线程池事先准备好一批线程,创建线程和销毁线程的资源就没有了
2.使用线程池缩短任务的执行时间。
有一个新的任务就new 一个线程,那么时间消耗为:New Thread() T1:线程的创建时间,T2:任务的执行时间 ,T3:线程的销毁时间
准备好一堆的线程准备好 就不需要T1 T3
3.线程是稀缺而昂贵的资源,因为线程创建出来消耗CPU,消耗内存(一定消耗内存),线程执行太多,多操作系统是一种负担,使用某种机制把线程统一管理
2.线程池的创建
最顶层的接口Executor
作用:把任务的提交和任务的执行做一个拆分
public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
真正意义上线程池的接口:ExecutorService
最常用的ThreadPoolExecutor
ThreadPoolExecutor构造函数的参数很多
所有参数有7个之多
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
......
}
这些参数各有各的作用,而且根据你配置参数的不同对于线程的执行或者工作的机制有比较大的影响
各个参数的含义(各个参数对工作机制的影响):
int corePoolSize: 当前线程池的核心线程数
int maximumPoolSize: 最大线程数,当前线程池所能使用的最大线程数量
long keepAliveTime: 控制存活时间,如果当前线程数目超过了corePoolSize,线程没事做空闲下来的线程,通过keepAliveTime、unit来控制这些线程存活的时间
TimeUnit unit: 控制存活时间的单位(s/ms)
BlockingQueue workQueue: 线程池是用来处理任务的,如果任务数大于线程数,放到阻塞队列中
ThreadFactory threadFactory: 可以适当对创建线程时做一点点微调工作,给线程定个名字
RejectedExecutionHandler handler: 拒绝策略,如果任务实在太多了,超过于最大线程和阻塞队列,就对超出的任务使用拒绝策略
一启动当前有3个线程,线程刚诞生,提交任务时,corePoolSize里面的线程一个一个接任务
如果提交了3个任务,corePoolSize满了,又提交一个任务,线程池不会马上启动一个线程,这个任务会进阻塞队列
如果阻塞队列也满了,还再提交任务,那么线程池会新启线程去执行任务
如果最大线程数也满了,那么拒绝策略就派上用场
3.拒绝策略
拒绝策略是一个接口
public interface RejectedExecutionHandler {
/**
* Method that may be invoked by a {@link ThreadPoolExecutor} when
* {@link ThreadPoolExecutor#execute execute} cannot accept a
* task. This may occur when no more threads or queue slots are
* available because their bounds would be exceeded, or upon
* shutdown of the Executor.
*
* <p>In the absence of other alternatives, the method may throw
* an unchecked {@link RejectedExecutionException}, which will be
* propagated to the caller of {@code execute}.
*
* @param r the runnable task requested to be executed
* @param executor the executor attempting to execute this task
* @throws RejectedExecutionException if there is no remedy
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
JDK为我们提供了四种拒绝策略(饱和策略)
1.DiscardOldestPolicy in ThreadPoolExecutor
直接丢弃最老的那一个,队列最前面的扔了
2.AbortPolicy in ThreadPoolExecutor
直接抛出异常,默认策略
3.CallerRunsPolicy in ThreadPoolExecutor
让调用者线程去执行任务
4.DiscardPolicy in ThreadPoolExecutor
把最新提交的任务直接丢弃
也可以自定义实现RejectedExecutionHandler 接口,把它作为参数交给自己定义的线程池
4.提交任务 (execute 和 submit)
1.execute方法(ThreadPoolExecutor里面实现的)接受的参数是Runnable,(不关心有没有返回结果)
2.submit方法(AbstractExecutorService里面实现的)接受Callable参数,即使是接受Runnable参数也要返回一个Future这个参数,(提交任务,需要拿到返回结果)
5.关闭线程池
shutdown (尝试关闭线程池,中断没执行任务的线程,一般都成功)
shutdownNow(不管有没有在执行,都尝试中断,正在执行任务的线程不一定会成功被终端)
线程的中断是协作机制,看怎么处理中断信号的
6.合理配置线程池
对任何线程池的配置一定要首先区分任务的特性
1.CPU密集型
MaximumPoolSize (最大线程数)不要超过CPU的核心数
线程太多CPU不停计算还有不停地切换,有用的时间就做无用的工作了
Runtime.getRuntime().availableProcessors(); 当前可用核心数是多少
最多情况,顶多加一个1 ,因为内存是有限的,会在磁盘开辟虚拟内存,等数据从硬盘到内存页缺失状态,如果出现页缺失情况,为了CPU充分利用+1,保证任意时刻都是有线程在CPU上运行的
2.IO密集型
机器的核心数*2
网络通信、读写磁盘的速度要远远低于CPU和内存读写速度的,为了避免CPU等数据的情况,让别的线程先用,避免CPU空闲
IO操作基本不用CPU,DMA机制,cpu操作io设备时向磁盘控制器或者网络控制器发送信号要做什么事情,你做完了 通知我,做完后会发送中断信号告诉cpu做完了
补充零拷贝的概念:
程序运行在OS上面,OS在机器上运行, OS提供内核空间和用户空间,用户应用不能访问OS内核程序,OS提供接口你去访问设备
从网卡到OS数据不是CPU控制的了,交给网络控制器了
用户不能访问网卡,网卡把数据交给OS,然后OS交给用户,数据之间的拷贝
用户-》拷到OS空间-》发到网卡
网卡-》OS空间-》拷贝用户空间
允许用户申请一部分空间 ,放在这里 ,读取这里 ,不会再有拷贝的过程
3.混合型
相差不太大,那么拆分,差分成CPU密集型和IO密集型
相差很大,密集型花费5ms,网络操作花费5s等待,就不必拆分,视为io密集型.
通过网络读取时间、内存读取时间来、CPU时间来判断怎么做,要不要拆分
Jeff Dean 在Google全体工程大会的报告有一份数据可供参考
WorkQueue:
尽量配置成有界的
corePoolSize :
看是不是忙,忙的话 大一些 ,不忙小一些