线程池概念及应用
线程池是为了省去频繁创建线程所带来的开销,统一管理线程,在程序一开始就创建一些线程供程序使用,使用完毕后不会结束该线程,而是放回线程池,以便被再次使用,可以理解为线程池是一个线程的集合,每一个线程都可被多次使用。
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序
都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。
线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。
如果一个线程的时间非常长,就没必要用线程池了(不是不能作长时间操作,而是不宜。),况且我们还不能控制线程池中线程的开始、挂起、和中止。
线程池原理
线程池的创建源码分析
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
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:线程池核心线程数量,即初始线程数
- maximumPoolSize:线程池最大线程数量
- keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
- unit:存活时间的单位
- workQueue:存放任务的队列
- handler:超出线程范围和队列容量的任务的处理程序
线程池中任务的执行流程
在创建线程池的过程中,最终会调用以上构造方法去创建线程,可根据不同的线程池策略使用参数进行调整。在创建完成后,将一些线程任务加入到线程池中后,线程池的处理流程如下:
- 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
- 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
- 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
工作队列中的任务在什么时候被执行
先查看以下源码:
while (task != null || (task = getTask()) != null) {
task.run();
}
摘选自java.util.concurrent.ThreadPoolExecutor.runWorker(Worker w);方法。代码片段,其中getTask()是从任务队列(workQueue)中获取一个任务,有兴趣的话可以查看JDK完成源码。
由此可以看出,线程池只要有线程执行结束,就会从队列中继续取出任务执行,所以队列中的任务只要线程池有线程空闲,就会被执行。
线程池的分类
Executor框架的最顶层实现是ThreadPoolExecutor类,Executors工厂类中提供的newFixedThreadPool、newCachedThreadPool等方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。
Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。经查看源码如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
在我们调用ExecutorService threadPool = Executors.newCachedThreadPool();获取一个新的线程池时,实际上又调用了ThreadPoolExecutor的构造器,返回的是一个ThreadPoolExecutor对象。从参数上看,可缓存线程池原理上是;
- 核心池大小也就是初始线程数为0;
- 最大线程数为Integer.MAX_VALUE,可理解为不限制大小;
- 每个线程可存活60秒,即无任务执行状态下,60秒即被销毁;
- 使用SynchronousQueue作为工作队列,即不保存元素,直接使用最大可用线程数创建线程。
newFixedThreadPool
创建一个固定大小的线程池,可根据参数控制核心线程数和最大线程数:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
可使用ExecutorService threadPool = Executors.newThreadPoolExecutor (4);创建一个固定长度的线程池,以经验来看,可根据系统资源来控制线程池的大小,一般使用机器的处理器核心数,可通过Runtime.getRuntime().availableProcessors()获取处理器核心数。
由源码可以看出,固定长度的线程池的创建原理为:
- 通过参数来控制了核心线程数和最大线程数,即核心线程数等于最大线程数,当核心线程数已满后,新任务只能存储在并发队列中。
- 每个线程在无任务的状态下,存活时间为0,即立即销毁。
并发队列使用LinkedBlockingQueue,并且未指定队列长度,即不限制长度。
newScheduledThreadPool
创建一个定长可调度线程池,支持定时及周期性任务执行
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler);
}
static class DelayedWorkQueue extends AbstractQueue<Runnable>
implements BlockingQueue<Runnable> {…}
可以看出,newScheduledThreadPool方法返回值与其他的不同,是ExecutorService的子接口ScheduledExecutorService,可以使用ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4)创建一个可调度的线程池,通过以上源码,最终依然是调用ThreadPoolExecutor的构造器去创建线程池,只不过使用了自定义的一个工作队列,这个工作队列可调度线程执行任务。
由以上四组源码可以看出,可调度的线程池实现原理是:
- 通过参数指定核心线程数
- 不限制最大线程数
- 当线程无任务时,立即销毁
使用DelayedWorkQueue作为工作队列。
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
newSingleThreadExecutor
创建一个单一线程的线程池,所有任务按顺序依次执行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
通过 ExecutorService threadPool = Executors.newSingleThreadExecutor()创建单一线程的线程池,核心线程数和最大线程数都为1,说明线程池中只能有一个线程,多余的任务存储在LinkedBlockingQueue中,但核心线程执行完后,依次执行。
饱和策略
当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。JAVA提供了4中策略:
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:只用调用所在的线程运行任务
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
在以上介绍的线程池中,CachedThreadPool和ScheduledThreadPool都不限制最大线程数,FixedThreadPool和SingleThreadExecutor都不限制队列大小,所以并不会出现饱和的情况。只有在自定义线程池中才有可能使用到饱和策略。
以第四种饱和策略为例,使用如何方案定义饱和策略,并将返回值的handler作为参数传入ThreadPoolExecutor的构造器中即可。
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();
合理配置线程池
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
- 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
- 任务的优先级:高,中和低。
- 任务的执行时间:长,中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。