java优雅定制线程池
Posted 高级JAVA指南
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java优雅定制线程池相关的知识,希望对你有一定的参考价值。
作为java老司机的阁下是否经常用到线程池?那你是否有了解线程池的使用规范?线程池的基本原理?如何根据实际情况优雅地定制一个合适线程池?
本文会着重介绍线程池的使用规范、以及如何根据实际情况优雅地定制线程池,关于讲述线程池的基本原理的文章在头条和各大IT平台上有很多,本文只粗略地概括一下并附上推荐文档的链接。
1.线程池基本原理
1).线程池基本架构
线程池继承体系
2).线程池主要方法说明:
java.util.concurrent.ExecutorService是java线程池框架的主要接口,用Future<V>保存任务的运行状态及计算结果,主要方法有:
void execute(Runnable) 提交任务到线程池
Future<?> submit(Runnable) 提交任务到线程池并返回Future
Future<T> submit(Runnable, T) 提交任务到线程池并返回Future, 第二个参数会作为计算结果封装到Future
Future<T> submit(Callable<T>)提交任务到线程池并返回Future,call方法的计算结果会封装到Future
List<Future<T>> invokeAll(Collection<? extends Callable<T>>) 批量提交任务并返回计算结果
T invokeAny(Collection<? extends Callable<T>> tasks) 只执行其中一个任务并返回结果
void shutdown() 线程池不再接受新任务,继续运行正在执行中的任务及等待中的任务
List<Runnable> shutdownNow() 线程池不再接受新任务,继续运行正在执行中的任务,返回待执行的任务列表
awaitTermination(long, TimeUnit) 阻塞线程直到线程池中的任务执行完毕或超时
3).线程池任务调度说法简述:
ThreadPoolExecutor是java线程池框架(Executor-> ExecutorService)的主要实现类,其中定义了线程池的状态,线程池的任务调度策略,任务饱和策略,线程的创建和销毁策略等。
任务调度算法:
a.如果当前线程池线程个数小于corePoolSize则开启新线程
b.否则添加任务到阻塞队列
c.如果任务队列满了,则尝试新开启线程执行任务,如果线程>maximumPoolSize则执行RejectedExecutionHandler。
再问各位一个问题?ThreadPoolExecutor是如何实现线程复用的?
2.线程池使用规范
在实际开发中如果需要并发地或异步地去执行一些任务时你是如何实现的?是new Thread(task)还是通过线程池实现?一般地推荐使用线程池的方式,这一点在阿里巴巴的Java开发手册中有强制要求,如下图:
阿里巴巴Java开发手册之线程池使用说明
那么为什么要使用线程池呢?上图中已基本说明原因,简单地说就是,使用线程池有两大优点:
a).线程池可以节约系统资源,包括线程、内存资源等,这样可以避免创建过多的线程导致线程资源匮乏、系统频繁进行上下文切换以及内存溢出等问题,因为线程池中的每一个线程可能会轮询地执行多个任务。
b).线程池可以节省重新创建线程的时间,进而提高响应速度。
了解了线程池的优点之后再来看线程池的创建和使用规范:
1).线程池的线程命名要贴近具体业务:这会给通过日志去定位、分析问题带来便利。
jdk自带的线程池的线程命名方式是:"pool-" + poolNumber.getAndIncrement() + "-thread-" + threadNumber.getAndIncrement() ,即"pool-" + 线程池计数+ "-thread-" +当前线程池中的线程数计数,效果如"pool-1-thread-n"。很明显这种命名方式无法区分具体业务。
那么如何指定线程池中线程的命名方式呢?一种是自己实现java.util.concurrent.ThreadFactory接口并新增入参传入namePrefix用以区分不同业务,一种是使用现成的如spring的org.springframework.scheduling.concurrent.CustomizableThreadFactory。
2).合理分配线程数:应根据任务的并发量、是否为计算密集型任务、机器的CPU核数合理分配线程数。N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。
3).合理选择任务队列并限制任务队列大小:
一般推荐使用LinkedBlockingQueue, Executors.newFixedThreadPool使用的就是它,LinkedBlockingQueue是链表实现的单向阻塞队列,可以指定队列容量上限,入队和出队使用了两把锁,这在高并发时有显著的性能优势。反之如果不指定队列大小则当系统并发较高时会堆积大量请求,这可能会导致内存溢出。
其他可选的任务队列类型有:
ArrayBlockingQueue:数组实现的有界阻塞队列。
SynchronousQueue:不存储元素的阻塞队列。Executors.newCachedThreadPool使用该队列。
DelayQueue:优先级队列实现的无界阻塞队列。需要延迟调度或定时调度时可选。
PriorityBlockingQueue:优先级排序的无界阻塞队列。
LinkedTransferQueue:链表实现的无界阻塞队列。
LinkedBlockingDeque:链表实现的双向无界阻塞队列。
4).合理选择线程池饱和时的任务拒绝策略:
jdk已实现的线程池的拒绝策略有:
AbortPolicy(默认):抛出异常使线程池服务挂掉。
DiscardPolicy:什么都不做。
DiscardOldestPolicy:弹出最队首的任务然后提交新任务。
CallerRunsPolicy:由调用者运行一次任务的run方法。
任务的拒绝策略应根据任务的重要程度选择不同的拒绝策略,丢弃重要的任务要谨慎(这时候可能需要进行系统优化或升级硬件配置),无足轻重的任务可以直接丢弃,但丢弃任务时什么都不做(如选择DiscardPolicy)是有风险的,这会使系统丢弃了一些任务但你还不知道。如果选择默认的AbortPolicy当达到线程池的上限时会抛出异常使线程池服务挂掉,这可能并不是预期的。
如果已知的拒绝策略并没有合适的,可以选择自己实现拒绝策略,如自己实现一个根据任务的重要程度去打印拒绝任务的日志或发送邮件告警等。
5).合理选择任务的提交方式:向线程池提交任务有两种方式:调用 submit方法或调用execute,如果无需返回值则应该使用execute提交任务,否则应该使用submit提交任务。
6).合理选择线程池类型:如果不需要获取各子任务的计算结果则使用ExecutorService的实现ThreadPoolExecutor即可,如果需要获取各个子任务的计算结果并合并的话就需要建议使用ExecutorCompletionService,如果需要定时调度任务则建议使用ScheduledExecutorService。
3.优雅定制线程池
基于线程池的使用规范,这里通过代码演示的方式举例如何优雅地定制线程池。
1).简单的创建线程池的方式:
例如我要通过线程池向缓存中写入第三方的数据,那么线程池可以这样创建:
new ThreadPoolExecutor(cacheCorePoolSize, cacheMaxPoolSize, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(500), new CustomizableThreadFactory("exec-outside-cache-"), new LogPolicy());
即分别制定核心线程数为8,最大线程数为64,空闲线程的存活时间为60s, 任务队列为容量为2000的LinkedBlockingQueue,线程命名方式为通过spring中的CustomizableThreadFactory指定线程命名为"exec-outside-cache-n",指定线程池的饱和拒绝策略为自定义的拒绝任务时打印日志(包含线程池的线程数配置,队列大小配置,线程池分组名称)。
当然上面这些具体的参数配置需要结合实际情况,如果公司的机器配置较高,那相应的线程数和队列大小可以再调高一点。
创建好线程池之后就可以向线程池提交任务了,因为我无需关注任务的返回结果所以选择execute方式提交任务。
2).本文内容涉及链接:
JAVA线程池配置及使用注意事项
自定义线程池饱和拒绝策略及线程池使用方法封装及测试:
https://github.com/stathry/commons/blob/master/src/main/java/org/stathry/commons/utils/Executors2.java
https://github.com/stathry/commons/blob/master/src/test/java/org/stathry/commons/concurrent/Exec2Test.java
https://github.com/stathry/jdkdeep/blob/master/src/org/stathry/jdkdeep/concurrent/exec/ExecutorServiceTest.java
更多代码用例可以通过以下网站搜索获得:
https://www.programcreek.com/java-api-examples/
http://grepcode.com/
文章写的不好的地方欢迎老司机指正或点评。如果觉得有收获记得关注哦!
最后回答一下文中提到的一个问题,ThreadPoolExecutor是如何实现线程复用的?
是在ThreadPoolExecutor#runWorker中起了一个while循环然后不断地去任务队列中poll或take。
以上是关于java优雅定制线程池的主要内容,如果未能解决你的问题,请参考以下文章
如果优雅地关闭ExecutorService提供的java线程池