在JDK1.5中JUC引入了线程池,也给我们编程带来了极大的方便,在阿里的Java开发手册中也强制要求线程资源必须通过线程池提供,不允许在应用中显示创建线程。可以减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题,如果不使用线程池,可能会造成系统创建大量同类线程,导致内存过度。但是如果使用线程池不恰当的设置参数会无法达到预期的效果。
线程池的实现:
Executors.newSingleThreadExecutor();
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
Executors.newCachedThreadPool();
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
Executors.newFixedThreadPool(10);
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
Executors.Executors.newScheduledThreadPool(10);
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
阿里的java开发手册中明确提示过不允许使用Executors去创建线程池,而应该通过ThreadPoolExecutor的方式去,这样的方式可以更加明确的了解线程池的运行规则,
FixedThreadPool和SingleThreadPool:允许请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
CachedThreadPool和ScheduledThreadPool:允许创建线程数量为Integer.AX_VALUE,可能创建大量的线程,从而导致OOM。
使用ThreadPoolExecutor创建线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
构造器中各个参数的含义:
- corePoolSize:核心线程池的大小。在创建线程池后,默认线程池找那个没有任何线程,而是等待有任务到来才创建线程去执行任务。(prestartAllCoreThreads()或者prestartCoreThread()方法)
- maximumPoolSize:线程池中最大线程数,表示线程池中最多能创建多少个线程
- KeepAliveTime:表示线程没有任务执行时保持多长时间终止 queueCapacity:任务队列容量(阻塞队列)
- RejectedExecutionHandler :拒绝策略
4种拒绝策略:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出
- RejectedExecutionException异常
- ThreadPoolExecutor.CallerRunsPolicy:也是丢弃任务,但是不抛出异常
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.DiscardPolicy:由调用线程处理该任务
线程池的执行任务过程如下:
- 当系统中线程数量小于核心线程数时,会创建线程。
- 当系统中线程数量大于核心线程数,并且任务队列未满(queueCapacity未满),将任务放入阻塞队列。
- 当系统中线程数量大于核心线程数,并且任务队列已满,先判断线程数是否小于最大线程数,如果小于就创建线程。如果等于最大线程数,则抛出异常,并执行拒绝策略。
如何合理的设置线程池的大小:
首先看两个概念:
- CPU密集型:也叫计算机密集型,指系统的硬盘、内存比CPU要好很多,大部分情况下,系统运作时CPU的使用率100%,CPU在I/O读写时很快就可以完成。
- IO密集型:和CPU密集型相反,指CPU的性能比内存、硬盘好很多,系统运行时大部分时间在进行读写操作,CPU使用率不是很高。
对于CPU密集型任务应该配置尽可能小的线程,比如CPU数+1个线程数,IO密集型任务应该配置尽可能多的线程,因为IO操作不占用CPU,不要上CPU闲下来,如CPU数*2+1个线程数。
最佳线程数 = ((线程等待时间 + 线程CPU时间)/ 线程CPU时间)* CPU核数
=(线程等待时间与线程CPU时间之比 + 1)* CPU核数
比如:CPU运行时间0.5S,线程等待时间(IO消耗时间)1.5S,CPU核数8,则最佳线程数=((1.5+0.5)/0.5)*8=32
由此可见:线程等待时间占比越高,需要线程越多。CPU时间占比越高,需要线程越少。
还有一种方法设置是:
tasks:每秒需要处理的任务数;taskcost:每个任务花费时间;responsetime:系统容忍最大相应时间
核心线程数=每秒需要多少线程处理
=tasks/(1/taskcout)=tasks*taskcout
考虑到二八法则还要
阻塞队列容量=(corepoolsize/taskcout)*responsetime
最大线程容量=(max(tasks)-阻塞队列容量)/(1/taskcost)
是否使用线程池就一定比单线程效率高呢?
答案是否定的,比如redis就是单线程的,但是却十分高效, 据说理论上操作可以达到百万量级/s。本质原因在于redis都是基于内存操作,可以更高效地利用CPU,而多线程会带来上下文切换开销,单线程并没有这种开销。多线程适用的场景一般是:存在相当比例的IO和网络操作。
可以关注公众号,每天一道大厂面试题,大家一起学习,一起成长。
参考资料: http://ifeve.com/how-to-calculate-threadpool-size