池化技术在我们的项目中是使用很频繁的,除了之前数据库的连接池,还有处理任务的线程池,在本文中将会来研究线程池的使用和原理.
人类认识事物的方式有以下几步:是什么,能干什么,怎么用,原理.
所以本文将通过以上几步来逐步学习.
在以前的单线程项目中,我们执行业务逻辑的流程是串行化的,一个main方法,然后依次调用其他的方法,这整个执行的流程是有序的.
在后来,我们发现使用Thread类可以启动多线程来帮助我们处理不同的任务,比如三个任务A,B,C,他们的调用关系是A->B->C,其中A和C耗时1s,C任务比较复杂,耗时2秒.那么此时如果我们用单线程的话,总共耗时需要4s.
如果我们使用Thread的方式来创建多线程去单独运行B任务的话,这样就会在2s内完成整个任务流程.
以上的方式相信大家都能理解.
现在问题来了,如果我们的任务很多,每次调用都要去new Thread
来启动一个线程,那么随着任务数量的增多,我们的Thread会越来越多,会严重影响服务器的性能.在这种情况下,池化技术为我我们实现了线程池来帮助我们维护众多的线程.
所谓线程池,就是在一个池中维护很多的线程,当我们要使用的时候就取出一个线程,用完以后不销毁,放回池中,这样就可以保证线程的复用,降低系统因为创建和销毁线程带来的资源损耗.
线程池的工作主要是控制运行的线程数量,处理过程中将任务放入阻塞队列,然后在线程创建后启动这些任务.
特点:
在java中,线程池就是Executor,但是在使用的时候,我们并不是直接来用这个接口,而是通过一个工具类:Executors来创建线程池.
通过Executors
创建的线程池有很多种,在这里我们需要知道以下三种:
Executors.newFixedThreadPool()
创建一个固定线程数量的线程池
Executors.newSingleThreadPool()
创建一个只有一个线程的线程池
Executors.newCachedThreadPool()
创建一个线程数量可变的线程池
我们可以写简单的代码来熟悉线程池的使用:
Executor executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 运行...");
});
}
以上代码中,我们创建了5个线程的线程池,提交10个打印任务到线程池,下面是打印结果:
pool-1-thread-2 运行…
pool-1-thread-3 运行…
pool-1-thread-1 运行…
pool-1-thread-4 运行…
pool-1-thread-4 运行…
pool-1-thread-4 运行…
pool-1-thread-3 运行…
pool-1-thread-2 运行…
pool-1-thread-5 运行…
pool-1-thread-1 运行…
可以看到,10个任务都是被5个线程轮询完成的.
虽然JDK为我们提供了Executors类来创建线程池,但是不建议使用,尤其在阿里巴巴java开发手册中明确说明,不应该使用Executor来创建线程池,应该使用ThreadPoolExecutor
来创建线程池.
那么ThreadPoolExecutor
是什么呢?我么一起来看一下
我们知道,创建线程池都可以用Executors
来new不同类型,我们看一下源码会发现其实他底层用的都是一个类来实现的:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
在这三种创建线程池的方法中,底层都是使用ThreadPoolExecutor
来创建的,在它的构造方法中,有很多的参数,我们来看看这些参数的意思.
在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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
下面是每个参数的意思:
根据以上的参数配置,ThreadPoolExecutor会为我们创建一个线程池,该线程池底层的工作原理如下:
exeute()
方法以后,线程会做如下判断:
具体的流程图如下:
具体的饱和拒绝策略有一下四中:
RejectedExecutionExeception
异常阻止系统正常运行.其实虽然JDK为我们实现了很多的线程池,但是在实际的使用中我们还是会自定义的来使用.那么在实际生产环境下,对于maximumPoolSize
我们是如何配置的呢?
一般来说,会按照以下的方式配置最大线程数量:
CPU密集型任务
如果该任务需要大量的运算,而没有阻塞,那么这个时候应该配置尽可能少的线程数量.如代码在while循环中运算
数量: CPU核数+1 个线程的线程池.
IO密集型任务*
因为IO密集型任务并不是一直在执行任务,则应该配置尽可能多的线程,如CPU核数*2
此类任务会大量阻塞,因此需要更多的线程数.
数量: CPU核数/1-阻塞系数
阻塞系数在0.8~0.9之间
比如8核CPU: 8/1-0.9 = 80个线程数.
在本篇文章中,对JDK的ThreadPoolExecutor
的使用做了介绍,在实际的工作中我们应该根据服务器以及业务的具体情况来使用线程池.