默认的线程池使用起来有不少的风险,比如容易OOM,所以一般我们不使用系统的默认的线程池,需要自己根据业务特点来定制线程池。
定制线程池无非是关注线程池的几个参数
一 核心线程数
核心线程数即corePoolSize ,如果是cpu密集的任务,核心线程数设置为cpu核心的1-2倍,如果是io密集的任务,设置的线程数为2倍以上,具体可以根据公式评估下:
《Java并发编程实战》的作者 Brain Goetz 推荐的计算方法:
线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)
通过这个公式,如果我们任务的等待时间比较长(IO密集型),那么线程数就多,如果平均工作时间比较长(CPU密集型),那线程数就少。
线程数少了无法充分利用系统资源,线程数多了调度频繁,上下文切换比较多,反而会降低性能,具体设置多少,我们还可以通过监控JVM线程数和cpu的负载情况,来设定合适的线程数。
最大线程数一般设置为核心线程数的几倍,以便可以达到更好的应对突发的情况。
二 阻塞队列
阻塞队列我们可以选择java线程池里面常用的几个队列:LinkedBlockingQueue 或者 SynchronousQueue 或者 DelayedWorkQueue。不过更推荐使用ArrayBlockingQueue,这个队列内部是通过数组来实现的,它最大的特点是有容量限制,一旦设置了不可以修改,如果线程池的线程数量已经达到了最大线程数,且队列也满了,后续的任务都会按照拒绝策略进行拒绝了,但是对于无限制增加线程数或者队列容量不限制的场景,ArrayBlockingQueue队列加上限制了最大线程数据,防止了程序占用内存太多,而导致的OOM问题。
如果我们使用容量更大的队列和更小的最大线程数,就可以减少上下文切换带来的开销,但也可能因此降低整体的吞吐量;如果我们的任务是 IO 密集型,则可以选择稍小容量的队列和更大的最大线程数,这样整体的效率就会更高,不过也会带来更多的上下文切换。
三 线程工厂
定制线程工厂的目的是为了让线程名变的可以识别,可以根据任务名称来设置线程名。
比如可以通过com.google.common.util.concurrent.ThreadFactory
Builder 来实现,如代码所示。
ThreadFactoryBuilder builder = new ThreadFactoryBuilder();
ThreadFactory calFactory = builder.setNameFormat("cal-pool-%d").build();
这样生成的线程名称为: cal-pool-1,cal-pool-2等
四 拒绝策略
默认的实现的四种策略AbortPolicy,DiscardPolicy,DiscardOldestPolicy 或者 CallerRunsPolicy 我们一般会选择CallerRunsPolicy 策略或AbortPolicy更合适点,还可以通过实现RejectedExecutionHandler 接口来实现自己的拒绝策略 ,如下:
private static class CustomMyRejectionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 打印 临时存储啊
}
}
我们通过实现rejectedExecution这个接口来自定义拒绝策略。
网友评论