线程池作用
- 线程池,能起到复用资源,提高响应速度,提高线程可管理性的作用。
execute和submit
-
execute和submit,都是向jdk线程池提交任务的方法,前者只能提交无返回值的任务(runnable),后者还支持提交带返回值的任务(callable)。
-
处理任务异常时,execute方法会直接抛出异常,需要try-catch处理。submit 会吃掉异常,并在调用future.get方法时再抛出(future接口支持传入一个阻塞等待时间)。
-
executor 和 executorService 是jdk线程池的两个接口,前者只支持execute提交任务,后者还支持submit提交任务。实现类todo。ScheduledThreadPool xxx。
ThreadPoolExecutor
-
ThreadPoolExecutor是jdk线程池类,构造参数有:核心线程数,最大线程数,线程保活时间,阻塞队列,线程工厂和拒绝策略。默认执行构造函数后不会初始化线程。
-
当向线程池提交任务后,开始创建线程执行。继续提交任务,继续创建线程,直到达到核心线程数后,不再创建新线程。新提交的任务被放入阻塞队列中。如果阻塞队列被占满,会再次创建新线程来执行任务,直到线程数达到最大线程数。如果仍有任务提交,且阻塞队列还被占满,就会执行拒绝策略。
-
调用 prestartAllCoreThreads() 能立即创建所有核心线程。调用 allowCoreThreadTimeOut(true) 设置核心线程空闲指定时间也会被回收。
-
线程保活时间,是指核心线程创建后默认不会被销毁,但大于核心线程数的线程会在闲置保活时间后被销毁。指定线程工厂,能自定义创建线程过程,如指定线程名等参数。
-
阻塞队列常用实现,先进先出的有界队列 ArrayBlockingQueue 和 LinkedBlockingQueue(默认大小 Integer.MAX_VALUE),优先级无界队列PriorityBlockingQueue,支持延时的无界队列 DelayQueue,无存储空间的队列 SynchronousQueue,其存入必须等待获取操作。链表结构的无界队列 LinkedTransferQueue,链表结构的双端阻塞队列LinkedBlockingDeque。
-
任务拒绝策略默认是丢弃任务并抛出异常,其他策略:丢弃任务但不抛出异常,丢弃等待最久的队首任务后加入新任务,使用提交任务的线程来执行等。通过实现 RejectedExecutionHandler 接口能自定义拒绝策略。
-
ThreadPoolExecutor 创建的线程池,只有在阻塞队列占满后才会扩容线程数到最大,这在一些场景中是扩容不及时的,为此可以重写阻塞队列,让其插入元素后就返回队列满的假象,这样能触发立即扩容。tomcat 线程池就实现了类似的效果,不等阻塞队列满先扩容线程。
线程池大小
-
指定线程池大小,需要根据任务类型来指定,大量计算操作的cpu密集型任务,建议线程数设置为(cpu个数+1)。大量文件,网络 io操作的任务,建议线程数设置为(2*cpu个数)。多一个线程可以完成将数据从硬盘读到内存的工作。
-
获得硬件的 cpu 个数,使用 Runtime.getRuntime().availableProcessors()。
-
可以使用一个公式来计算:((w+c)/c)nu,w是等待时间,c是cpu计算时间,n是cpu核数,u是cpu利用率。
-
实际运用时需要通过压测和监控来确定合理大小,保证合理响应时间内TPS最高。
-
不同任务类型或执行时间长短差别大的任务建议单独创建线程池,避免相互影响。注意 Java8 中的 parallel stream 背后是共享的同一个 ForkJoinPool。
任务队列
- 线程池设置任务队列通常是有界的,只需要队列大小稍大于预估需求即可,这样既不会导致扩容滞后,也不会造成正常情况下拒绝任务。
Executors
-
线程池创建工具类,不推荐使用,底层仍是 ThreadPoolExecutor。
-
newCachedThreadPool 方法,是创建最大线程数是 Integer.MAX_VALUE 和无存储阻塞队列 SynchronousQueue 的线程池。
-
newFixedThreadPool 方法,是创建一个线程数固定,无界阻塞队列 LinkedBlockingQueue 的线程池。
-
上述两种线程池都非常容易出现 OOM,线程占用资源过载和阻塞队列过载。
-
new SingleThreadPool 方法,是创建只有一个线程的线程池。todo
-
new ScheduledThreadPool 方法,是创建固定线程数的线程池,并支持定时,周期性的执行任务;todo
优雅关闭
-
ThreadPoolExecutor 的 shutdown 和 shutdownNow 都是关闭线程池的方法,区别是 shutdown 不接收新任务并等待队列中的任务执行完成后关闭,而 shutdownNow 是不接收新任务,并中断正在执行的任务和清空队列中的任务。
-
两个方法中其一被调用后,线程池处于 shutdown 状态,如果线程池中资源都销毁成功,会处于 terminate 状态。
-
优雅停机的实践,应该先调用 shutdown,然后多次调用 awaitTermination(time) 来阻塞等待线程池关闭,如果指定时间内该方法返回 true 表示线程池正常关闭,否则返回false,此时也不能无限期等待下去,因此可以兜底调用 shutdownNow 来尽可能做资源回收。
-
shutdown 和 shutdownNow 都是异步关闭线程池,await 是同步阻塞等待线程池关闭。
-
没有绝对的无损停止,不能无限制等待 shutdown,使得整个应用无法停机,因此重点是做好任务异常中止的业务兜底,因为还要考虑硬件故障,掉电等无法触发优雅停机的场景。
线程池监控
-
通过继承 ThreadPoolExecutor,并重写 beforeExecute,afterExecute,terminated 方法,可以在任务执行前,任务执行后和线程池关闭前,加入特定的监控逻辑,如监控任务的平均执行时间,最大执行时间,和最小执行时间等。
-
ThreadPoolExecutor 还提供了一些监控的属性:总任务数 taskCount,完成任务数 completedTaskCount,曾创建的最大线程数largestPoolSize 和当前存活的线程数 activeCount 等。
-
开源监控有 DynamicTp。
使用注意
-
任务异常处理:可以在任务代码使用try-catch处理,或future对象的get方法处理异常,以及为工作线程设置exceptionHandler来处理(自定义线程工厂)。
-
上下文中参数传递,可以使用鹰眼EagleEye,底层是threadLocal。
-
线程池扩展有 guava 的实现。
Fork/Join 框架
- Fork/Join 框架是 java7 提供的并行执行任务的框架,通过把大任务分割成若干个小任务,最终汇总每个小任务的结果后得到大任务的结果。