线程复用:线程池
线程池总概
什么是线程池?
接触过JDBC的人,一定听说过数据库连接池(比如,c3p0、Druid等)。其实在我的理解中,两者是差不多的。不过线程池中放的是线程而已。
线程是一种轻量级工具,但其创建与关闭都需要花费一定的时间。而且大量的线程会抢占内存资源。盲目的大量资源会对系统造成极大的压力。
线程池,中有一定数量的活跃线程。创建线程变成了从线程池中获得空闲线程;关闭线程变成了向线程池归还线程。
JDK对于线程池的支持
Java通过Executors提供五种线程池,分别为:
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。newSingleThreadScheduledExcutor
创建单线程化的线程池,支持定时及周期性任务执行。
线程池的使用
首先是简单使用,这个没有什么特殊之处。
只需记得newFixedThreadPool
创建的是定长的线程池,可控制线程最大并发数,超出的线程会在队列中等待。
而newCachedThreadPool
创建的线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
定时任务
newScheduledThreadPool
支持定时及周期性任务执行,查看了其源码,主要有以下三种方法:
- schedule():在给定时间,对任务进行调度;
- scheduleAtFixedRate() 和 scheduleWithFixedDelay():对任务进行周期性调度,但两者有所区别。
scheduleAtFixedRate() 和 scheduleWithFixedDelay() 的区别
- 两种调度的区别:
- FixedRate 方式:以上一个任务开始执行时间为起点,在之后的延迟时间后,调用下一次任务。
- FixedDelay 方式:上一个任务结束后,再经过延迟时间进行任务调度。
- 若任务执行时间超过调度时间,
- FixedRate 方式:若调度时间过短,那么任务会在上一个任务结束后立刻调用(不会出现任务堆叠的现场)。
- FixedDelay 方式:会严格按照
任务间隔时间 = 调度时间 + 任务执行时间
。
如果任务遇到异常,那么后续的所有子任务都会停止调度。因此必须保证,异常被及时处理,为周期性任务的稳定调度提供条件。
关于线程池的记录
拒绝策略
创建线程池的核心类 ThreadPoolExecutor 有一个参数指定了拒绝策略。拒绝策略,是系统超负荷运行时的补救措施,通常是由于压力太大而引起的,也就是线程池中的线程已经用完了且等待队列已经排满了。
JDK 提供了四种拒绝策略:
- AbortPolicy 策略:直接抛出异常,阻止系统正常工作。
- CallerRnsPolicy 策略:只要线程池未关闭,将直接在调用者线程中运行被丢弃的任务。这种做法不会真的丢弃任务,但是任务提交线程的性能将急剧下降。
- DiscardOldestPolicy 策略:丢弃最老的一个请求,也就是即将被执行的任务(处于等待队列的队头),并尝试再次提交当前任务。
- DiscardPolicy 策略:直接丢掉无法处理的任务。
- 自定义策略:自己扩展 RejectedExecutionHandler 接口。
线程扩展
ThreadPoolExecutor
是一个可扩展的线程池,有beforeExecute()
、afterExecute()
和terminated()
能够对线程进行控制。
protected void beforeExecute(Thread t, Runnable r) { } protected void afterExecute(Runnable r, Throwable t) { } protected void terminated() { }
这是三个protected
的空方法,摆明了可以让子类扩展。
* 在执行任务的线程中将调用beforeExecute
和afterExecute
等方法,在这些方法中还可以添加日志、计时、监视或者统计信息收集的功能。
* 无论是正常运行,还是抛出异常,都会调用afterExecute
。但是,如果抛出Eorror,将不会调用该方法;或者beforeExecute
抛出一个RuntimeException
,则任务将不被执行,即该方法也不会被调用。
* 关于terminated
,在线程池完成关闭时(就是在所有任务已经完成且所有工作者线程已经关闭),用来释放Executor
在生命周期里分配的各种资源,此外还能执行信息通知、日志记录等功能。
补充
1. 使用线程池被”吃”掉了异常堆栈信息
在使用线程池提交线程时,可能会发生异常堆栈信息被”吃”掉的现象,而解决方法:
- 放弃submit(),改用execute()。
-
获取submit()方法返回类的get()方法。
-
-
Future future = pools.submit(new Thread()); future.get();
-
3. 扩展 ThreadPoolExecutor 线程池,让其在调度任务前,先保存提交任务线程的堆栈消息(就是重写线程池线程的调用方法)。
2.自定义线程:ThreadFactory
这个接口只有一个方法 newThread(Runnable r)
,主要是由线程池调用新建线程。
3. 优化线程池线程数量
在《Java Concurrency in Practice》书中给了一个估算线程池大小的经验公式(同时,在Java中,可以通过Runtime.getRuntime().availableProcessors
获取可用的CPU数量。)。
Ncpu = CPU数量 Ucpu = 目标CPU的使用率,0 <= Ucpu <= 1 W/C = 等待时间与计算时间的比率 所以,最优的线程池大小为: Nthreads = Ncpu * Ucpu * ( 1 + W/C )