多线程的软件设计方法确实可以最大限度地发挥现代多核处理器的计算能力,提高生产系统的吞吐量和性能。
但是,若不加控制和管理地随意使用线程,对系统的性能反而会产生不利的影响。
因此,生产实际中我们通常会用线程池来管理线程的创建。
无限制创建线程的缺陷
首先,线程虽然与进程相比是一种轻量级的工具,但是创建和关闭依然需要花费时间,如果非常频繁的创建和销毁线程,很可能出现创建和销毁线程的耗时大于该线程真实工作所消耗的时间,反而会得不偿失。
其次,线程本身也是要占用内存空间的,如果不控制线程的数量,大量的线程会抢夺宝贵的内存资源,如果处理不当,可能会导致Out of Memory异常,即便没有,大量的线程回收也会给GC带来很大的压力,延长GC的停顿时间。
因此,对线程的使用必须要掌握一个度,在有限的范围内,增加线程的数量可以明显提高系统的吞吐量,但是超过了这个范围,大量的线程会拖垮应用系统。
JDK并发Executor框架
为了节省系统在多线程并发时不断创建和销毁线程所带来的额外开销,就需要引入线程池。线程池的基本功能就是进行线程的复用。JDK提供了一套Executor框架,帮助开发人员有效的进行线程控制,其核心成员有Executor、ExecutorService、AbstractExecutorService、ThreadPoolExecutor,ThreadPoolExecutor继承了AbstractExecutorService类,AbstractExecutorService类实现了ExecutorService接口,ExecutorService的父级接口Executor,因此通过这个接口任何Runnable或Callable的对象都可以被ThreadPoolExecutor调度。
成员继承体系如下图:
以上成员均在java.util.concurrent包中,此外还有Executors工具类,Executors类扮演着线程池工厂的角色,通过Executors可以获取一个特定功能的线程池,下面是Executors工厂类的主要方法:
//固定大小的线程池
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>());
}
//单个线程定时线程池
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
//定时线程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
以上工厂方法分别返回具有不同工作特性的线程池,这些线程池工厂方法的具体说明如下:
- newFixedThreadPool(int nThreads)方法
该方法返回一个固定线程数的线程池,该线程池中的线程数量始终不变。当有一个新任务提交时,线程池中若有空闲线程,则立即执行,若没有,则新的任务会被暂时存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 - newSingleThreadExecutor()方法
该方法返回一个只有一个线程的线程池。若多余的一个任务提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按照先入先出的顺序执行队列中的任务。 - newCachedThreadPool()方法
该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有的线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有的线程在当前任务执行完毕后,将返回线程池进行复用,当空闲线程在指定的时间内(60s)无执行任务时被回收。 - newSingleThreadScheduledExecutor()方法
该方法返回一个ScheduledExecutorService对象,线程池大小为1。ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间执行某任务的功能,如果在某个固定的延时之后执行,或者周期性执行某个任务。 - newScheduledThreadPool(int corePoolSize)方法
该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量。
阿里巴巴JAVA开发手册关于创建线程池有这样的描述:
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
为什么阿里大厂强制不允许使用Executors创建线程池呢,我们再回顾一下Executors工厂类创建的线程池的方法,无论是newFixedThreadPool(int nThreads),newSingleThreadExecutor()或者是newCachedThreadPool()创建的线程池的方法,其内部均实现使用了ThreadPoolExecutor的构造函数,从以上线程池的实现代码可以看到,他们都是对ThreadPoolExecutor类的一个封装。
为何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;
}
函数参数含义如下:
- corePoolSize 指定线程池中的核心线程数量
- maximumPoolSize 线程池中允许的最大线程数
- keepAliveTime 线程空闲时的存活时间 即超过corePoolSize的空闲线程在多长时间会被销毁
- unit keepAliveTime的时间单位
- workQueue 任务队列,用来保存被提交但尚未被执行的任务
- threadFactory 线程工厂,一般用默认即可,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名
- handler 拒绝策略 当任务太多来不及处理,如何拒绝任务
以上参数中,大部分都很简单容易理解,至于workQueue 任务队列,threadFactory 线程工厂,handler 拒绝策略的详细内容由于篇幅过长如果想了解可以移步博客尾部参看另一篇博客。
回到为什么阿里大厂强制不允许使用Executors创建线程池这个问题呢,我们回顾一下newFixedThreadPool(int nThreads)方法实现。它返回了一个corePoolSize和maximumPoolSize大小一样的,并且使用了LinkedBlockingQueue任务队列的线程池。因为对于固定大小的线程池而言,不存在线程数量上的动态变化,因此corePoolSize和maximumPoolSize可以相等。同时它使用无界队列存放无法立即执行的任务。当任务提交非常频繁的时候,该队列可能迅速膨胀,从而耗尽系统资源。
回顾一下newSingleThreadExecutor()方法实现返回的单线程线程池,是newFixedThreadPool(int nThreads)方法的一种退化,只是简单地将线程池的线程数量设置为1,等同于newFixedThreadPool(1)。
回顾一下newCachedThreadPool()方法实现返回的corePoolSize为0,maximumPoolSize为无穷大的线程池,这意味着在没有任务时,该线程池内无线程,而当任务被提交时,该线程池会使用空闲的线程执行任务。若无空闲线程,则将任务加入SynchronousQueue队列,而SynchronousQueue队列时一种直接提交的队列,它总会迫使线程池增加新的线程执行任务。当任务执行完毕后,由于corePoolSize为0,因此空闲线程又会在指定时间内(60秒)被回收。对于newCachedThreadPool(),如果同时有大量任务被提交,而任务的执行又不那么快时,那么系统便会开启等量的线程处理任务,这样做可能会因为过多创建线程耗尽系统资源。
ThreadPoolExecutor 相关友情链接
- ThreadPoolExecutor 构造函数参数解析
参数解析包含了workQueue任务队列,threadFactory线程工厂,handler拒绝策略参数详解与案例 - ThreadPoolExecutor构造函数自定义线程池
Spring Boot项目通过构造函数方式自定义线程池案例与线程池大小数量优化控制 - Java扩展线程池追踪任务执行信息以及耗时情况
自定义一个线程池继承了ThreadPoolExecutor,然后重写beforeExecute,afterExecute,terminated方法实现对线程池运行状态的跟踪,了解线程池的具体使用情况以及每个线程实行耗时信息。