线程池是一种 “池化” 的线程使用模式,通过创建一定数量的线程,让这些线程处于就绪状态来提高系统响应速度,在线程使用完成后归还到线程池来达到重复利用的目标,从而降低系统资源的消耗。
线程池的优势
- 降低资源消耗
通过重复利用已创建的线程降低线程创建和销毁造成的消耗 - 提高响应速度
当任务到达时,任务可以不需要等到线程创建就能立即执行 - 提高线程的可管理性
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控 - 提供更多更强大的功能
线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池 ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行
创建线程池
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
在Java中可以通过ThreadPoolExecutor类创建线程池,通过这两种方式创建线程的时候需要向构造方法传递线程池参数,线程池参数的具体含义,后面会讲。Java也为我们提供了一些常用的线程池类供我们直接使用,可以通过如下几种方式创建一个线程池:
- Executors.newCachedThreadPool()
- Executors.newFixedThreadPool(int nThreads)
- Executors.newSingleThreadExecutor()
- …
通过上面的方式创建线程池,不需要我们显式的指定线程池参数,而是Java来完成,其本质上Java还是通过ThreadPoolExecutor类去创建线程池。
不过,创建线程池对象,还是强烈建议通过使用ThreadPoolExecutor的构造方法创建,不要使用Executors。阿里《Java开发手册》中的一段描述:
【强制】线程池不允许使用Executors创建,建议通过ThreadPoolExecutor的方式创建,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors返回的线程池对象的弊端如下:
1.FixedThreadPool和SingleThreadPool: 允许的请求队列长度为Integet.MAX_VALUE,可能会堆积大量的请求从而导致OOM;
2.CachedThreadPool: 允许创建线程数量为Integet.MAX_VALUE,可能会创建大量的线程,从而导致OOM.
使用自定义线程工厂:当项目规模逐渐扩展,各系统中线程池也不断增多,当发生线程执行问题时,通过自定义线程工厂创建的线程设置有意义的线程名称可快速追踪异常原因,高效、快速的定位问题。
使用自定义拒绝策略:虽然,JDK给我们提供了一些默认的拒绝策略,但我们可以根据项目需求的需要,或者是用户体验的需要,定制拒绝策略,完成特殊需求。
线程池划分隔离:不同业务、执行效率不同的分不同线程池,避免因某些异常导致整个线程池利用率下降或直接不可用,进而影响整个系统或其它系统的正常运行。
线程池参数
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
corePoolSize(线程池核心线程大小)
当提交一个任务时,如果当前核心线程池的线程个数没有达到 corePoolSize,则会创建新的线程来执行所提交的任务,即使当前核心线程池有空闲的线程。如果当前核心线程池的线程个数已经达到了 corePoolSize,则不再重新创建线程。 -
maximumPoolSize(线程池能创建线程的最大个数)
如果当阻塞队列已满时,并且当前线程池线程个数没有超过 maximumPoolSize 的话,就会创建新的线程来执行任务。 -
keepAliveTime(空闲线程存活时间)
如果当前线程池的线程个数已经超过了 corePoolSize,并且线程空闲时间超过了 keepAliveTime 的话,就会将这些空闲线程销毁 -
unit(时间单位)
为 keepAliveTime 指定时间单位。 -
workQueue(阻塞队列)
用于保存任务的阻塞队列。 -
threadFactory(创建线程的工厂类)
可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。 -
handler(拒绝策略)
后续可以专门写一篇文章模拟一下各种拒绝策略的运行流程
ThreadPoolExecutor内部有实现4个拒绝策略:
(1)、CallerRunsPolicy:由调用execute方法提交任务的线程来执行这个任务;
(2)、AbortPolicy:默认的拒绝策略,抛出RejectedExecutionException异常,拒绝提交任务;
(3)、DiscardPolicy:不做任何处理,直接抛弃任务;
(4)、DiscardOldestPolicy:去除任务队列中的第一个任务(最旧的),重新提交;
线程池状态
demo示例
package com.example.threadpool;
import java.util.concurrent.*;
/**
* @Author: Alan
* @Date: 2023/2/3 16:15
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor executorService= new ThreadPoolExecutor(10, 20,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(10));//自定义线程
for (int i = 1; i <= 100; i++) {
executorService.execute(new MyTask(i));
}
}
}
class MyTask implements Runnable {
int i = 0;
public MyTask(int i) {
this.i = i;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程处理第" + i + "个任务");
try {
Thread.sleep(2000L);//业务逻辑
} catch (Exception e) {
e.printStackTrace();
}
}
}
在开始讲execute方法执行流程前我们先看上面这个demo。在这个demo中,我们创建了一个具有10个核心线程,线程数量最多为20个,的线程池,同时这个线程池阻塞队列使用LinkedBlockingQueue来实现,大小为10。
此外,我们循环100次,向线程池提交100个任务,最后我们来看下控制台会输出什么样的结果呢?
通过打印出来的结果,我们知道发现,1-10个任务最先执行,其次是21-30个任务,在此期间还抛出了一个异常(后面解释),最后执行的是11-20个任务。
execute
创建线程池后,通过调用该方法,即可将创建的任务交给线程池处理。
该方法主要做了三件事儿:
- 根据核心线程数判断是否执行addWorker方法创建线程
- 当前线程池中运行的线程数大于核心线程数时,则会将提交的任务放入到阻塞队列中
- 当阻塞队列已满,且当前线程池中运行的线程数介于核心线程数和线程池能创建线程的最大个数之间时,执行addWorker方法创建线程,反之执行拒绝策略。
addWork
该方法主要完成两件事儿,一、根据线程池的状态和当前线程池中运行的线程数,决定能否创建新的线程。二、如果通过了一的判断,那么可以创建新的线程,同时执行start方法开启线程。
如果允许创建线程,那么会创建一个对线程做了包装的Worker对象(Worker对象实现了Runable接口,同时持有我们所提交的的任务,即firstTask对象)。
当我们调用Woker的start方式时,会执行Worker中的run方法。
runWorker
该方法是一个比较核心的方法,从这里我们可以到线程池执行任务的优先级,同时也可以看到线程池中的任务究竟在何时开始执行。
4. 获取要执行的任务
> 优先执行当前worker对象持有的任务,阻塞队列中的任务优先级最低。
**线程池优先从传入的worker对象上去拿要执行的任务,取到就执行该任务,没有取到则会调用getTask()方法从阻塞队列中获取任务**。getTask方法负责从阻塞队列的对头获取任务。这些任务都是在执行execute方法时存入的(execute方法的执行流程,前面已经做过分析)。

- 调用任务重写的run方法
线程池获取到任务时会调用任务中重写的run方法,在刚方法中即可处理我们的业务逻辑。
该方法中核心逻辑被放入到一个while循环中执行,每一次执行完task.run()方法后,都会将task置空,然后开始新一轮的循环(再次从阻塞队列中获取新的任务),这里体现了线程池线程复用的设计思想。
如何配置线程池的线程数
参考阅读:合理配置线程池的线程数量
明白了线程池的运行原理后,在生产实践中我们该知道如何合理配置线程池参数,从而使得线程池高效运行。我们将需要线程池执行的任务分为CPU密集型(计算密集型)、IO密集型、混合型三类。
CPU密集型
CPU密集型任务是指计算机在执行某个任务时,需要CPU进行大量的计算工作或者逻辑判断工作(比如一些算法的处理),执行任务期间CPU一直处于运行状态,例如执行一个任务需要10秒,其中有9秒CPU都在运行,剩下的1秒可能是做IO操作或者其他不需要使用CPU的操作。
可见,上述的这类任务对于单位时间内CPU的利用率是非常高的,CPU几乎满负荷运行。因此,在处理这类任务时,我们无需将线程池的最大线程数设置的特别大,设置的太大反而可能增加额外的切换开销。
参数设置推荐:最大线程数=CPU核心数+1;核心线程数=最大线程数*20%
IO密集型
IO密集型任务是指计算机在执行某个任务时,大量时间均花费在通过磁盘、内存或者网络读取数据上,在此期间我们的线程是处于阻塞状态的,而CPU也是空闲的,例如执行一个任务需要10秒,其中有9秒都在做IO操作,剩下的1秒才需要用到CPU。
可见,上述的这类任务对于单位时间内CPU的利用率是非常低的,CPU几乎是空闲的。因此,在处理这类任务时,我们需要将线程池的最大线程数设置的大一些,这样当线程阻塞,CPU空闲时,可以让更多的线程获得CPU的使用机会。
参数设置推荐:最大线程数=CPU核心数2;核心线程数=最大线程数20%
当然具体如何合理设置线程池值大小,需要结合系统实际情况,在大量的尝试下比较才能得出,以上只是别人总结的规律,并不具有针对性。
混合型
混合型任务,指既包含CPU密集型,又包含IO密集型。
对于此类程序,我们可以将任务划分成cpu密集型任务与IO密集型任务,分别针对这两种任务使用不同的线程池去处理。这样我们就可以针对这两种情况对线程池配置不同的参数。