hi,我是蛋挞,一个初出茅庐的后端开发,希望可以和大家共同努力、共同进步!
开启掘金成长之旅!这是我参与「掘金日新计划 · 4 月更文挑战」的第 29 天,点击查看活动详情
为什么用线程池?
线程池可以看做是管理了N个线程的池子,和连接池类似。线程池的作用主要有:
- 控制并发数量:线程并发数量过多,抢占系统资源从而导致阻塞,线程池可以限制线程的数量。
- 线程的复用:创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率和速度
- 管理线程的生命周期:对线程进行一些简单的管理,创建,销毁等
认识线程池?
线程池继承体系
在Java 1.5之后就提供了线程池 ThreadPoolExecutor,它的继承体系如下:

- ThreadPoolExecutor :线程池
- Executor: 线程池顶层接口,提供了execute执行线程任务的方法
- Execuors: 线程池的工具类,通常使用它来创建线程池
private static void fixedThreadPool() {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0 ; i < 150 ; i++){
executorService.execute(new Runnable() {
@Override
public void run() {
//有5个线程在执行
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":线程执行...");
}
});
}
}
线程池原理[重点]
执行流程
一个任务在线程池中是如何执行的?生活举例:
- 老陈要开软件公司,合伙几个核心的程序员做开发 :(线程核心数)
- 新的项目过来一个人接收一个项目去做,没有人手了,把新进来的项目放入项目排队池(任务队列)
- 如果项目队列中的任务过多,需要招聘一些临时的程序员(非核心线程),但是规定所有的开发总人数不能50(最大线程数)
- 如果新的项目进来,核心程序员和临时程序员都没有人手了,并且项目队列也放满了,新来的项目该如何处理呢?①拒绝 ②丢弃老的项目做新的项目 ③老陈自己做新的项目
线程池执行流程 : 核心线程 => 等待队列 => 非核心线程 => 拒绝策略
- 如果有空闲的线程直接使用,没有空闲的线程并且线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
- 线程数量达到了corePoolSize,则将任务移入队,列等待空闲线程将其取出去执行(通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源,整个getTask操作在自旋下完成),如果队列已满,新建线程(非核心线程)执行任务,空闲下来以后,非核心线程会按照时间策略进行销毁
- 队列已满,总线程数又达到了maximumPoolSize 最大线程数,就会执行任务拒绝策略。
举例:
1请求过来 -> 分配一个核心线程去处理
第6个任务过来 ,5个核心线程都在忙-> 把新的任务放入队列(SynchronousQueue除外,会直接创建线程)
第 16个任务过来,核心在忙,队列已满 -> 创建普通线程处理新任务
第21个任务过来 ,核心在忙,普通线程在忙,队列已满 -> 拒绝策略
如果任务做完,普通线程超过空闲时间,就会被销毁
线程池核心构造器
线程池源码 ThreadPoolExecutor 构造器:

PS: 线程池7个参数的构造器非常重要[重要]
- CorePoolSize: 核心线程数,不会被销毁
- MaximumPoolSize : 最大线程数 (核心+非核心) ,非核心线程数用完之后达到空闲时间会被销毁
- KeepAliveTime: 非核心线程的最大空闲时间,到了这个空闲时间没被使用,非核心线程销毁
- Unit: 空闲时间单位
- WorkQueue:是一个BlockingQueue阻塞队列,超过核心线程数的任务会进入队列排队
- SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务;
- LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
- ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小
- ThreadFactory:使用ThreadFactory创建新线程。 推荐使用Executors.defaultThreadFactory
- Handler: 拒绝策略,任务超过 最大线程数+队列排队数 ,多出来的任务该如何处理取决于Handler
- AbortPolicy丢弃任务并抛出RejectedExecutionException异常;
- DiscardPolicy丢弃任务,但是不抛出异常;
- DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务;
- CallerRunsPolicy由调用线程处理该任务
可以定义和使用其他种类的RejectedExecutionHandler类来定义拒绝策略。
常见四种线程池
Jdk官方提供了常见四个静态方法来创建常用的四种线程.可以通过Excutors创建
- CachedThreadPool:可缓存
- FixedThreadPool :固定长度
- SingleThreadPool:单个
- ScheduledThreadPool:可调度
Excutors这个工具类中,util是工具类,以后见到以s结尾也是工具类.Collections Arrays Paths等
CachedThreadPool
可缓存线程池-可以无限制创建

根据源码可以看出:
- 这种线程池内部没有核心线程,线程的数量是有限制的 最大是Integer最大值。
- 在创建任务时,若有空闲的线程时则复用空闲的线程(缓存线程),若没有则新建线程。
- 没有工作的线程(闲置状态)在超过了60S还不做事,就会销毁。
- 适用:执行很多短期异步的小程序或者负载较轻的服务器。
实战
private static void cachedThreadPool() {
//带缓存的线程,线程复用,没有核心线程,线程的最大值是 Integer.MAX_VALUE
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0 ; i < 150 ; i++){
executorService.execute(new Runnable() {
@Override
public void run() {
//始终只有一个线程在执行
System.out.println(Thread.currentThread().getName()+":线程执行...");
}
});
}
}
FixedThreadPool 定长线程池

根据源码可以看出:
- 该线程池的最大线程数等于核心线程数,所以在默认情况下,该线程池的线程不会因为闲置状态超时而被销毁。
- 如果当前线程数小于核心线程数,并且也有闲置线程的时候提交了任务,这时也不会去复用之前的闲置线程,会创建新的线程去执行任务(必须达到最大核心数才会复用线程)。如果当前执行任务数大于了核心线程数,大于的部分就会进入队列等待。等着有闲置的线程来执行这个任务。
- 适用:执行长期的任务,性能好很多。
private static void fixedThreadPool() {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0 ; i < 150 ; i++){
executorService.execute(new Runnable() {
@Override
public void run() {
//有5个线程在执行
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":线程执行...");
}
});
}
}
SingleThreadPool

根据源码可以看出:
- 有且仅有一个工作线程执行任务
- 所有任务按照指定顺序执行,即遵循队列的入队出队规则。
- 适用:一个任务一个任务执行的场景。 如同队列
//单线程的线程池
private static void singleThreadExecutor() {
//单线程的线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0 ; i < 10 ; i++){
executorService.execute(new Runnable() {
@Override
public void run() {
//始终只有一个线程在执行
System.out.println(Thread.currentThread().getName()+":线程执行...");
}
});
}
}
ScheduledThreadPool

根据源码可以看出:
- DEFAULT_KEEPALIVE_MILLIS就是默认10L,这里就是10秒。这个线程池有点像是CachedThreadPool和FixedThreadPool 结合了一下。
- 不仅设置了核心线程数,最大线程数也是Integer.MAX_VALUE。
- 这个线程池是上述4个中唯一一个有延迟执行和周期执行任务的线程池。
- 适用:周期性执行任务的场景(定期的同步数据)
private static void scheduledThreadPool() {
//带缓存的线程,线程复用,没有核心线程,线程的最大值是 Integer.MAX_VALUE
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
//延迟 n 时间后,执行一次,延迟任务
executorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println("延迟任务执行.....");
}
},10, TimeUnit.SECONDS);
//定时任务,固定 N 时间执行一次 ,按照上一次任务的开始执行时间计算下一次任务开始时间
executorService.scheduleAtFixedRate(()->{
System.out.println("定时任务 scheduleAtFixedRate 执行 time:"+System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},1,1,TimeUnit.SECONDS);
//定时任务,固定 N 时间执行一次 ,按照上一次任务的结束时间计算下一次任务开始时间
executorService.scheduleWithFixedDelay(()->{
System.out.println("定时任务 scheduleWithFixedDelay 执行 time:"+System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},1,1,TimeUnit.SECONDS);
}
自定义ThreadPoolExecutor
private static void customThreadPool() {
//核心 4 个 ,最大 10 个 ,60s的空闲销毁非核心6个线程, 队列最大排队 10 个 = 最多同时处理 20个拒绝
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 10,
60L, TimeUnit.SECONDS, //最大空闲时间
new ArrayBlockingQueue<Runnable>(10), //队列排队10个
new ThreadPoolExecutor.DiscardPolicy()); //任务满了就丢弃
for (int i = 0 ; i < 210 ; i++){
int finalI = i;
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
//始终只有一个线程在执行
System.out.println(Thread.currentThread().getName()+":线程执行..."+ finalI);
}
});
}
}
在ThreadPoolExecutor类中几个重要的方法
- Execute :方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
- Submit :方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,实际上它还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。
- Shutdown :不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
- shutdownNow :立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
- isTerminated:调用ExecutorService.shutdown方法的时候,线程池不再接收任何新任务,但此时线程池并不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。在调用shutdown方法后我们可以在一个死循环里面用isTerminated方法判断是否线程池中的所有线程已经执行完毕,如果子线程都结束了,我们就可以做关闭流等后续操作了。
线程池中的最大线程数
一般说来,线程池的大小经验值应该这样设置:(其中N为CPU的核数)
如果是CPU密集型应用,则线程池大小设置为N+1,如果是IO密集型应用,则线程池大小设置为2N+1 32_2+1=65 64_2+1=129,如果一台服务器上只部署这一个应用并且只有这一个线程池,那么这种估算或许合理,具体还需自行测试验证。
但是,IO优化中,这样的估算公式可能更适合:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
因为很显然,线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
创建线程的个数是还要考虑 内存资源是否足够装下相当的线程
下面举个例子:
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。
总结
重点
- 线程池原理
- 线程池可以自定义,并且通过Excutors提供四个常用线程池
- 为什么要调优-项目上线设置变好,可用变大。程序在运行过程中出现问题。
- 通过java虚拟机参数的方式告诉jvm要使用多少内存,使用哪种垃圾回收器。
- jvm组成
- Jvm内存溢出 堆溢出 栈溢出 栈空间不足
- 判断对象已死 可达性分析
- 垃圾回收算法 分代回收:新生代(复制) 老年代(其他两种中一种)
- 常用垃圾回收器

线程池小结
- 为什么需要 防止线程过多导致系统崩溃 提高效率,节约资源
- 原理 核心线程 等待队列 非核心线程 最大线程数, 拒绝策略(拒绝最新并抛异常,拒绝最新,喜新厌旧,全部都执行)
- 怎么操作 Executors中提供始终基本线程池,就是四个静态方法。 cached fixed single sheched ================== 可以自定义 new ThreadPoolExecutor...
- 怎么确定最大线程数
- 理论上先设置一个 n+1 2n+1
- 实践后再进行计算