嗨喽~小伙伴们我来了,

上一章我们介绍了Java中的Thread类里一些常用的方法。本节我们就来聊一聊线程池。

说到“池”,大家或许都不陌生,在java中,我们有见过数据库连接池,Java常量池,对象池等等,将实体进行“池化”,这种“池化”思想,有助于我们对实体进行统一的管理,监控和调用。

本章的主要内容有:

  • 创建线程池
  • 构造方法的参数解读
  • 四种功能性线程池
  • 关闭线程池

作为经常被面试的一个模块,线程池的概念不是那么通俗易懂,但在实际开发应用中,线程池对程序性能优化有着不可磨灭的贡献。

首先,我们来对比下面两个程序运行的效率。

程序一,使用前面我们学过的一般线程:


import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.*;/*** @author sixibiheye* @date 2021/9/2* @apiNote 线程池初识*/
public class ThreadPoolDemo1 {public static void main(String[] args) throws InterruptedException {Long start = System.currentTimeMillis();Random random = new Random();List<Integer> list = new ArrayList<Integer>();for (int i = 0; i < 100000; i++) {Thread thread = new Thread( () -> {list.add(random.nextInt());});thread.start();thread.join();}Long end = System.currentTimeMillis();System.out.println("耗时:" + (end - start) + "ms");System.out.println("大小:" + list.size());}
}

简单理解就是创建100000个线程来对list添加数据,最后输出添加所需的总时间和 list 的大小。我们来看运行结果:

程序二,使用线程池(不理解的小伙伴们可以先跳过往下看):


import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;/*** @author sixibiheye* @date 2021/9/2* @apiNote 线程池初识*/
public class ThreadPoolDemo2 {public static void main(String[] args) throws InterruptedException {Long start = System.currentTimeMillis();Random random = new Random();List<Integer> list = new ArrayList<>();ExecutorService executorService = Executors.newSingleThreadExecutor();for (int i = 0; i < 100000; i++) {executorService.execute( () -> {list.add(random.nextInt());});}executorService.shutdown();executorService.awaitTermination(1, TimeUnit.DAYS);Long end = System.currentTimeMillis();System.out.println("耗时:" + (end - start) + "ms");System.out.println("大小:" + list.size());}
}

运行结果:

从耗时来看,使用线程池所需的时间比使用一般线程所需的时间足足减少了100多倍!由此看来,线程池对于程序的优化有着重大的意义。

实际上,从原理上理解,使用一般的线程,都需要经历创建,使用,销毁三个步骤,当创建的线程数非常大的时候,这种操作对内存的消耗比较大,导致效率低下。因此我们希望创建好的线程能够在指定时间内继续执行其他的任务,通过减少创建和销毁线程的消耗,以此来提高效率。

其实,线程池概念的提出与我们的生活有密切关系,许多算法,概念的提出都能够在生活中找到对应的例子。

如果要理解线程池,咱就必须提到“银行办理业务排队​​​​​​​”的场景逻辑,这是一个非常非常非常(重要的事情说三遍!!!)典型的例子,请小伙伴们务必看懂,对线程池的理解非常有帮助:

去过银行办理业务的朋友们都知道,银行里有多个窗口来办理业务,此外还有等候区供人们休息。我们现在假设有这样一个场景 :

如上图,假设现在某银行有3个窗口(1,2,3号),2个备用窗口(4,5号),和可供3人休息的等候区。

现在有1人来办理业务, 这1个人带着“任务1”去了1号窗口办理业务:

接着,第2,3个人也来办理业务,他们带着任务2,3分别去了2,3号窗口:

这时,如果银行来了第4个人,他只能去等候区等候,第5,6个人亦是如此:

此时,等候区人数已满,如果再来第7个人,银行行长只能开启备用窗口-----4号窗口,并让还在等候区等候的第4个人到4号窗口办理业务,等候区(队列)往前“挪一个”使得第7个人能够进入等候区:

同理,如果再来第8个人,那就只能开启第二个备用窗口-----5号窗口,并让第5个人到5号窗口办理业务,等候区(队列)往前“挪一个”使得第8个人能够进入等候区:

这时,不论是窗口数,还是等待区容量,都已经满了,如果来了第9个人,怎么办呢?

对于第9个人,银行只能采取拒绝的方式,因为就当前情况,不管是窗口,还是等候区,都容不下第9个人了。

这个便是一个简单的银行排队流程。借鉴于这种思想,我们把它搬到线程里,描述如下:

线程池(银行)里有最多5个线程数,有3个是核心线程(1,2,3号窗口),另外2个是备用线程(4,5号窗口,或者叫非核心线程),当核心线程全被使用后,就将多余的任务以队列的形式放入“任务队列”(等候区)中,如果任务队列也满了,就开启备用线程(非核心线程),如果备用线程也全部被使用了,那么剩下多余的任务,就只能拒绝执行了。

理解完上述过程,学习线程池,就轻松多了。在Java中,线程池的真正实现类是ThreadPoolExecutor,翻阅源码,它有如下几种构造方法:

上面四种构造方法中,最多的构造方法有七个参数,我们来看看这七个参数的具体含义:

1. corePoolSize (必需) : 核心线程数(类比银行的1,2,3号窗口)。默认情况下,核心线程会一直存活,除非将allowCoreThreadTimeout设置为true,这样超时后,核心线程也会被回收。

2. maxmumPoolSize (必需) : 最大线程数(类比银行的1,2,3,4,5号窗口)。对于非核心线程(4,5号窗口),在下面的keepAliveTime设定的时间超过之后,会被回收。同样的,将allowCoreThreadTimeout设置为true的话,这样超时后,核心线程也会被回收。

3. keepAliveTime (必需) : 如上,设定非核心线程的闲置时间,超时后,非核心线程会被回收。

4. unit (必需) : 指定上面keepAliveTime参数的单位,常用的有:

  • TimeUnit.MILLISECONDS(毫秒)
  • TimeUnit.SECONDS(秒)
  • TimeUnit.MINUTES(分)

5. workQueue (必需) : 任务队列(类比银行的等待区)。通过线程池中的execute()方法提交的Runnable对象将存储在该对象中。一般使用阻塞队列。

6. threadFactory (可选) : 线程工厂-----指定新线程创建的方式,自定义ThreadFactory的话可以修改线程名,线程组,优先级,是否为守护线程等等,如果不想自定义,使用默认的Executors.defaultThreadFactory()即可。

7. handler (可选) : 当线程池创建的线程数达到最大值时,需要执行的拒绝策略。

需要实现RejectedExecutionHandler接口,并重写

rejectedExecution(Runnable r , ThreadPoolExecutor executor) 方法。

Executors框架为我们提供了四种常见的拒绝策略:

  • 1.AbortPolicy (默认) :丢弃任务并抛出 RejectedExecutionException 异常
  • 2.CallerRunsPolicy :丢给调度线程处理该任务
  • 3.DiscardPolicy :丢弃任务但不抛出异常。一般用于自定义处理模式。
  • 4.DiscardOldestPolicy :丢弃队列最早的未处理完的任务,然后尝试执行新任务。​​​​​​​                        

请注意: 虽然指定了核心线程数和最大线程数,但是当线程池被创建后,线程不会立即创建,其会根据任务队列中是否有新任务要执行来实时地创建。

下面我们来认识一下 ThreadPoolExecutor 这个类,来看源码:

首先,最底层是一个函数式接口(只有一个抽象方法) Executor :

接着有一个叫 ExecutorService 的接口继承了 Executor ,其扩展了一些方法,如 isShutdown() , shutdown() , awaitTermination()等等:

然后,有一个叫 AbstractExecutorService 的类实现了 ExecutorService ,提供了一些方法的实现:

最后, 咱 ThreadPoolExecutor 类继承了 AbstractExecutorService ,并实现了 execute() 等重要的方法:

现在,我们来看一个简单的程序:


import java.util.concurrent.*;/*** @author sixibiheye* @date 2021/9/2* @apiNote 线程池*/
public class CustomThreadPool {public static void main(String[] args) {ExecutorService executorService = new ThreadPoolExecutor(25,50,1L, TimeUnit.SECONDS,new ArrayBlockingQueue<>(50),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());for (int i = 1; i <= 100; i++) {executorService.execute(new TaskDemo4(i));}//当所有任务执行完之后,结束线程池服务executorService.shutdown();}
}
class TaskDemo4 implements Runnable{private int i = 0;public TaskDemo4(int i){this.i = i;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "线程做了第" + i + "个任务");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}
}

上述程序中,通过for循环产生了100个任务,请大家细细体会 ThreadPoolExecutor() 构造方法中的7个参数如何取定。

如果已有任务数超过了线程池的最大线程数与任务队列容量之和,线程池就会执行拒绝策略,默认为上述第一种拒绝策略。比如将上述代码中的线程池创建参数---任务队列修改如下:

new ArrayBlockingQueue<>(49)

则会抛出 RejectedExecutionException 异常:

这七个参数中,大家比较模糊的是 workQueue ---- 任务队列。

下面我们来看看 workQueue 如何取定。任务队列是基于阻塞队列实现的,采用的是生产者-消费者模式,在Java中需要实现 BlockingQueue 接口,当然我们可以自定义实现类,但JDK已经为我们提供了7种阻塞队列的实现类,我们简单的介绍其中最常用的三种:

  • 1. ArrayBlockingQueue :一个由顺序表结构组成的有界(需指明容量)阻塞队列,
  • 2. LinkedBlockingQueue :一个由链表结构组成的阻塞队列。可以指明容量,未指明容量时,默认为无界(Integer.MAX_VALUE).
  • 3. SynchronousQueue :一个不存储任何元素的同步阻塞队列。

请注意有界队列与无界队列的区别:如果使用有界队列,当已有任务数超过了线程池的最大线程数与该队列容量之和后就会执行拒绝策略;而如果使用无界队列,队列容量无限大,已有任务数不可能超过该队列容量,所以设置 maxmunPoolSize 没有任何意义。

基于 ThreadPoolExecutor 的七个参数值的不同设定,Executors类 (Executor接口的工具类)给我们封装了几个常用的创建线程池的方法:

1. 可缓存线程池CachedThreadPool)方法源码:

  • 特点:无核心线程,非核心线程无限大,线程闲置60s后被回收,任务队列为不存储任何元素的同步阻塞队列
  • 适用场景:执行大量且耗时的操作

2. 定长线程池FixedThreadPool)方法源码:

  • 特点:只有核心线程,线程一旦闲置立即被回收,任务队列为链表结构的无界阻塞队列
  • 适用场景:需要控制线程最大并发数的地方

3. 定时线程池ScheduledThreadPool)方法源码:

  

  • 特点:核心线程固定,线程闲置10ms后被回收,任务队列为延时阻塞队列
  • 适用场景:执行定时或周期性的任务​​​​​​​

4. 单线程化线程池SingleThreadExecutor)方法源码:

  • 特点:核心线程固定为1个,没有非核心线程,线程一旦闲置立即被回收,任务队列为链表结构的无界阻塞队列
  • 适用场景:串行执行所有任务

总结起来,虽然使用Executors框架的4个功能线程池非常的方便,但是现在已经不建议使用了,而是采用最原始的方式:通过 ThreadPoolExecutor 来手动创建。

原因有两点:

1. 使用原始的 ThreadPoolExecutor 可以使我们更加明确线程池的运行机制,减少对资源的浪费。

2. 使用上述四种线程池还有自己的弊端:

  • FixedThreadPool  & SingleThreadExecutor :由于任务队列可以为无界队列,当任务过多时,可能会导致OOM(内存溢出)。
  • CachedThreadPool & ScheduledThreadPool :由于最大线程数为无限大,当线程创建过多时,可能会导致CPU利用率接近100%。

最后,我们来简单地介绍一下如何关闭线程池。

         在介绍如何关闭线程池之前,我们来看看线程池的五个状态:

我们来简单认识一下这五种状态:

1.RUNNING

特点:线程池处在 RUNNING 状态时,能够接收新任务,能够执行已添加的任务。

  • 线程池一旦被创建,就处于 RUNNING 状态,并且线程池中的任务数为0。

2.SHUTDOWN

特点:线程池处在 SHUTDOWN 状态时,不接收新任务,但能处理已添加的任务。

  • 调用线程池的shutdown()方法时,线程池状态转变:RUNNING --> SHUTDOWN

3.STOP

特点:线程池处在 STOP 状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。

  • 调用线程池的 shutdownNow()方法 时,线程池状态转变:( RUNNING or SHUTDOWN ) --> STOP

4.TIDYING

特点:当所有的任务都已中止或结束后,ctl记录的“任务数量”为0,线程池会变为 TIDYING 状态。

  • 当线程池在 SHUTDOWN 状态下,任务阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN --> TIDYING
  • 当线程池在 STOP 状态下,线程池中执行的任务为空时,就会由 STOP --> TIDYING

5.TERMINATED

特点:线程池彻底终止,就变成TERMINATED状态。

  • 线程池处在 TIDYING 状态时,执行完terminated()方法后,就会由 TIDYING --> TERMINATED

接着,查阅源码,我们可以看到,JDK在ThreadPoolExecutor类中提供了几种关闭线程池的方法,源码如下:

从上述源码结合前面学的知识可以发现:

  • 当线程池创建以后,初始时,线程池处于 RUNNING 状态,此时线程池中的任务为0;
  • 如果调用 shutdown() 方法,则线程池变为 SHUTDOWN 状态,此时线程池不能够接受新  的任务,它会等待所有任务执行完毕;
  • 如果调用 shutdownNow() 方法,则线程池处于 STOP 状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
  • 当所有的任务已中止或结束后,“任务数量”为0,线程池会变为 TIDYING 状态。接着会执行 terminated() 函数。
  • 线程池处在 TIDYING 状态时,执行完 terminated() 之后,线程池就被设置为TERMINATED状态。

其实,关于多线程,JDK中提供的Thread类ThreadPoolExecutor类中还有许多我们可以学习的东西,如果小伙伴们感兴趣的话可以去翻阅有关源码和资料。最后喜欢的小伙伴们点个赞鼓励支持一下吧~

Java多线程详解(线程池)相关推荐

  1. Java 多线程详解(五)------线程的声明周期

    Java 多线程详解(一)------概念的引入:https://blog.csdn.net/weixin_39816740/article/details/80089790 Java 多线程详解(二 ...

  2. Java多线程详解(线程不安全案例)

    嗨喽-小伙伴们我又来了, 通过前面两章的学习,我们了解了线程的基本概念和创建线程的四种方式. 附上链接: 1.  Java多线程详解(基本概念)​​​​​​​ 2. Java多线程详解(如何创建线程) ...

  3. Java 多线程详解(三)------线程的同步

    Java 多线程详解(一)------概念的引入:https://blog.csdn.net/weixin_39816740/article/details/80089790 Java 多线程详解(二 ...

  4. Java 多线程详解(二)------如何创建进程和线程

    Java 多线程详解(一)------概念的引入:https://blog.csdn.net/weixin_39816740/article/details/80089790 在上一篇博客中,我们已经 ...

  5. Java 多线程详解(四)------生产者和消费者

    Java 多线程详解(一)------概念的引入:https://blog.csdn.net/weixin_39816740/article/details/80089790 Java 多线程详解(二 ...

  6. 【运维能力提升计划-3】Java多线程详解

    Java多线程详解 学习链接 Java.Thread 线程简介 线程 进程 多线程 线程实现 Thread 继承Thread类 调用run方法只有主线程一个线程,调用start方法生成子线程与主线程并 ...

  7. Java多线程详解(基本概念)

    嗨喽-小伙伴们我来啦, 从本章开始,我们就要开始介绍Java中一个非常重要的概念-----多线程.线程化思想是计算机领域的重要思想,有了线程,咱编写的程序才能更为高效准确地运行起来. 首先,咱来了解一 ...

  8. Java多线程系列--“JUC线程池”06之 Callable和Future

    转载自  Java多线程系列--"JUC线程池"06之 Callable和Future Callable 和 Future 简介 Callable 和 Future 是比较有趣的一 ...

  9. Java多线程-新特性-线程池

    Sun在Java5中,对Java线程的类库做了大量的扩展,其中线程池就是Java5的新特征之一,除了线程池之外,还有很多多线程相关的内容,为多线程的编程带来了极大便利.为了编写高效稳定可靠的多线程程序 ...

  10. JAVA多线程详解(超详细)

    目录 一.线程简介 1.进程.线程 2.并发.并行.串行 3.进程的三态 二.线程实现 1.继承Thread类 2.实现Runnable接口 3.实现Callable接口(不常用) 三.线程常用方法 ...

最新文章

  1. Linux删除 指定数目行【或者所有行】删除光标到行首
  2. ETL工具大全,你了解多少
  3. win8/Metro开发系七 win8 对常见数据源的解析及处理 如:xml,json,以及html代码
  4. go mysql 查询语句_01 MySQL-初识MySQL-查询语句的执行流程-Go语言中文社区
  5. Linux-----diff命令
  6. otis电梯服务器tt使用说明_南充私人电梯
  7. 罗盘时钟编码代码_安全研究 | 利用macOS Dock实现代码的持久化执行
  8. 关于支付回调的一些思考
  9. springboot文件上传和下载工具_SpringBoot图文教程7—SpringBoot拦截器的使用姿势这都有...
  10. hutool-all 导入Excel 文件 学习笔记
  11. J 位操作练习 (Java)
  12. stm32获取绝对值编码器值(SSI,串行通讯)
  13. 马成荣版计算机应用基础 教案,课改理念在中职《计算机应用基础》教学中的应用...
  14. 日程安排工具Calendso
  15. 【华为云·云筑2020】云学院考卷答案
  16. 深度多模态子空间聚类网络+代码实现
  17. 查看页面滚动条滚动距离,可视区窗口尺寸
  18. 1.3(1) 框架——内嵌框架
  19. 即将到来的量子计算时代,其商业应用价值在哪里?
  20. 安信可 ESP8266 12F Flash操作

热门文章

  1. javascript中的时间处理
  2. Android 系统定时管理器AlarmManager的使用
  3. 【效率技巧】利用TI计算器的程序映射功能 kbdprgm1()~9() 简化GTC程序调试操作
  4. WCF 第一章 基础 为一个ASMX服务实现一个WCF客户端
  5. 活动目录的安装:深入浅出Active Directory系列(二)
  6. Part 2 —— 迁移到 Go Modules
  7. laravel 知识点总结
  8. 常用的实现Javaweb页面跳转的方式
  9. hdu5347 MZL's chemistry(打表)
  10. 与成都的幸福行动家交流GTD