Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >彻底搞懂 Java 线程池,干啥都不再发憷

彻底搞懂 Java 线程池,干啥都不再发憷

作者头像
一猿小讲
发布于 2020-07-27 15:19:26
发布于 2020-07-27 15:19:26
97500
代码可运行
举报
文章被收录于专栏:一猿小讲一猿小讲
运行总次数:0
代码可运行

作为 Java 程序员,无论是技术面试、项目研发或者是学习框架源码,不彻底掌握 Java 多线程的知识,做不到心中有数,干啥都没底气,尤其是技术深究时往往略显发憷。

1

回顾:创建线程的几种方式?

在 Java 的世界里,大家最熟悉的线程的创建方式,莫过于 Java 提供的 Thread 类和 Runnable 接口。

核心知识点(一):继承 Thread 类 VS 实现 Runnable 接口的区别?

从 JDK1.5 开始,Java 提供了 Callable 接口,提供另一种创建线程的方式。

核心知识点(二):实现 Callable 接口创建线程,有啥独特?

使用实现 Callable 接口的方式创建的线程,相对于继承 Thread 类和实现 Runnable 接口来创建线程的方式而言,可以获取到线程执行的返回值、以及是否执行完成等信息。

2

思考:无限制的创建线程,会带来什么问题?

在项目开发中,为了提高系统的吞吐量和性能,很多同学都会随手写出如下最简单的线程创建代码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
new Thread(new Runnable() {
    @Override    
    public void run() {  
          System.out.println("我是一个孤独的线程");        
          // do something    
    }
}).start();

有的同学,会采用 Lambda 表达式,写出如下稍显高 B 格的代码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
new Thread(() -> System.out.println("我是一个孤独的线程")).start();

在简单的应用中,上面的代码没有太大的问题。但是如果在业务量较大,可能要开启很多线程来处理,而当线程数量过大时,反而会耗尽 CPU 和内存资源,如果处理不当,可能会导致 Out of Memory 异常。

写段代码,简单示意一下内存溢出。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 待通知的消息
List<String> notifyMsgList = new ArrayList<String>(10000);
for (int i = 0; i < 10000; i++) {
    notifyMsgList.add("发工资啦" + i);
}
// 通知用户
for (String msg : notifyMsgList) {
    // 多线程发送    
    new Thread(new Runnable() {   
         @Override        
         public void run() {      
               System.out.println("通知:" + msg);            
               try {         
                      Thread.sleep(10000);            
               } catch (InterruptedException e) {          
                      e.printStackTrace();            
               }        
         }    
   }).start();
}

程序跑起来,大概率会出现内存溢出的异常。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
java.lang.OutOfMemoryError: unable to create new native thread

贴一效果图,真的不诳你。

这么看,线(劲)程(酒)虽好,不能贪多(杯)呀。在生产环境中,线程的数量必须要进行控制,不然盲目的创建大量线程会对系统性能造成伤害,甚至会导致内存溢出,拖垮应用。

那该怎么办?这就很有必要引入线程池啦。

3

Executor 框架入门:别造轮子啦,JDK 都给你提供啦!

线程池的基本功能就是进行线程的复用,当系统接受一个提交的任务,需要一个线程时,并不立即去创建线程,而是先去线程池查找是否有空余的线程,若有,则直接使用线程池中的线程进行工作,若没有,再去创建新的线程。待任务完成后,也销毁线程,而是将线程放入线程池的空闲对列,等待下次使用。

在线程频繁调度的场景中,JDK1.5 以前,攻城狮必须手动打造线程池,来节约系统开销;而从 JDK1.5 开始,Java 提供了一个 Excutors 工厂类来生产线程池,可以帮助攻城狮有效的进行线程控制。

核心知识点(一):Excutors 工厂类的主要方法有哪些?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newCachedThreadPool()
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

上面列举的方法中,分别返回了不同工作特性的线程池,下面简单介绍一下每个方法。

1. newFixedThreadPool(int nThreads)

用途:创建一个可重用的、具有固定线程数的线程池。

备注:该线程池中的线程数量始终不变,当有一个新的任务提交时,线程中若有空闲线程,则立即执行,若没有则空闲线程,新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

2. newSingleThreadExecutor()

用途:创建一个只有一个线程的线程池,相当于 newFixedThreadPool(int nThreads) 方法调用时传入的参数为 1。

备注:该线程池中的线程数量为 1。

3. newCachedThreadPool()

用途:创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程会被缓存在线程池中。

备注:该线程池中的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程,若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

4. newSingleThreadScheduledExecutor()

用途:创建只有一条线程的线程池,它可以在指定延迟后执行线程任务。

备注:线程池大小为 1,并且可以在固定的延时之后执行或者周期性执行某个任务。

5. newScheduledThreadPool(int corePoolSize)

用途:创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。

备注:corePoolSize 指池中所保存的线程数,即使线程是空闲的,也被保存在线程池内。

上面列举了 Excutors 类中的一些方法,仔细去看,会发现前三个返回一个 ExcutorService 对象,而后两个方法返回 ScheduledExecutorService 对象。

核心知识点(二):ExcutorService 与 ScheduledExecutorService 啥区别?

走进源码,从方法定义上着重分析一下区别。

首先截取 ExcutorService 的部分方法定义,重点关注 submit 重载的方法。

上面截图释义:

  • 标注 1 的方法释义:将一个 Callable 对象提交给指定的线程池,Future 代表 Callable 对象里 call 方法的返回值。
  • 标注 2 的方法释义:将一个 Runnable 对象提交给指定的线程池,result 显示指定线程执行结束后的返回值,所以 Future 对象将在 run 方法执行结束后返回 result。
  • 标注 3 的方法释义:将一个 Runnable 对象提交给指定的线程池,其中 Future 对象是 Runnable 任务的返回值(但是 run方法没有返回值,不过可以借用 Future 的方法来获取任务执行的状态)。

了解完 ExcutorService,那么来看看 ScheduledExecutorService 的方法定义。

上面截图释义:

  • 标注 1 的方法与标注 2 的方法,主要功能是在指定 delay 延迟后执行任务。区别是入参一个接受 Runnable 任务,一个接受 Callable 任务。
  • 标注 3 的方法,功能是在 initialDelay 后开始执行 command,而且以固定频率重复执行,依次是 initialDelay + period、initialDelay + 2*period ...。
  • 标注 4 的方法,功能是在 initialDelay 后开始执行 command,随后在每一次执行终止和下一次执行开始之前都存在给定的延迟,如果任务执行时遇到异常,就会取消后续执行。

了解完方法定义,区别就很简单了:

  • ExcutorService 代表一个线程池,可以执行 Runnable 对象或者 Callable 对象所代表的线程;
  • ScheduledExecutorService 是 ExcutorService 的子类,可以在指定延迟后执行线程任务或者周期性的执行某个任务。

4

Executor 框架使用:说的再多,都不如写一行代码!

还是以开篇发工资的场景为例,若借助线程池来实现,假设允许开启 10 个线程来进行发工资(理解成 10 个人同时干活就行),代码改造如下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 待通知的消息
List<String> notifyMsgList = new ArrayList<String>(10000);
for (int i = 0; i < 10000; i++) {
    notifyMsgList.add("发工资啦" + i);
}
// 1. 调用 Excutors 类的静态工厂方法创建一个 ExcutorService 对象(线程池);
ExecutorService executorService = Executors.newFixedThreadPool(100);
// 通知用户
for (String msg : notifyMsgList) {
    // 2. 创建 Runnable 实现类或者 Callable 实现类的实例,作为线程执行任务。    
    // 3. 调用 ExcutorService 对象的 submit 方法来提交 Runnable 实例或 Callable 实例。    
    executorService.submit(new Runnable() {    
        @Override        
        public void run() {      
              System.out.println("通知:" + msg);            
              try {         
                     Thread.sleep(10000);            
              } catch (InterruptedException e) {         
                     e.printStackTrace();            
              }        
        }    
   });
}
//4. 调用 ExcutorService 对象的 shutdown 方法来关闭线程池。
executorService.shutdown();

程序跑起来,很清晰的能看出有 10 个线程同时在处理任务。

好了,到这简单总结一下,使用线程池来编程时的步骤(仔细看代码注释就行啦)。

  • 调用 Excutors 类的静态工厂方法创建一个 ExcutorService 对象(线程池);
  • 创建 Runnable 实现类或者 Callable 实现类的实例,作为线程执行任务;
  • 调用 ExcutorService 对象的 submit 方法来提交 Runnable 实例或 Callable 实例;
  • 调用 ExcutorService 对象的 shutdown 方法来关闭线程池。

既然提到了关闭线程池的 shutdown 方法,那再抛一知识点:shutdown 与 shutdownNow 的区别是啥?

写段程序很容易发现结论,还是借助上面发工资通知的代码,把通知的消息改为 11,把线程数改为 10。

首先调用 shutdown 方法关闭线程池,代码如下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 待通知的消息
List<String> notifyMsgList = new ArrayList<String>(10000);
for (int i = 0; i < 11; i++) {
    notifyMsgList.add("发工资啦" + i);
}
// 1. 调用 Excutors 类的静态工厂方法创建一个 ExcutorService 对象(线程池);
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 通知用户
for (String msg : notifyMsgList) {
    // 2. 创建 Runnable 实现类或者 Callable 实现类的实例,作为线程执行任务。    
    // 3. 调用 ExcutorService 对象的 submit 方法来提交 Runnable 实例或 Callable 实例。    
    executorService.submit(new Runnable() {   
         @Override        
         public void run() {      
               System.out.println(Thread.currentThread().getName()+ "-->通知:" + msg);            
               try {        
                       Thread.sleep(10000);            
               } catch (InterruptedException e) {         
                      e.printStackTrace();            
               }        
         }    
   });
}
//4. 调用 ExcutorService 对象的 shutdown 方法来关闭线程池。
executorService.shutdown();

输出截图(执行完了,才优雅的终止):

接着改造代码,调用 shutdownNow 方法来关闭线程池,代码如下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//4. 调用 ExcutorService 对象的 shutdownNow 方法来关闭线程池。
List<Runnable> tasks = executorService.shutdownNow();
System.out.println(tasks);

输出截图:

结论:

  • 当程序调用 shutdown 方法时,线程池将不再接受新的任务,但会将以前所有已提交的任务执行完成(优雅停服的背后支撑者);
  • 当调用 shutdownNow 方法来关闭线程池时,该方法会试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。

5

寄语写最后

本次,本次主要回顾了创建线程的方式,并对 JDK 对任务执行框架 Excutor 进行初步讲解,后续会带你一起走进这个框架背后隐藏的 ThreadPoolExecutor 类,一起去探寻线程池背后的奥秘。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一猿小讲 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
线程池你真不来了解一下吗?
前言 只有光头才能变强 回顾前面: ThreadLocal就是这么简单 多线程三分钟就可以入个门了! 多线程基础必要知识点!看了学习多线程事半功倍 Java锁机制了解一下 AQS简简单单过一遍 Lock锁子类了解一下 本篇主要是讲解线程池,这是我在多线程的倒数第二篇了,后面还会有一篇死锁。主要将多线程的基础过一遍,以后有机会再继续深入! 那么接下来就开始吧,如果文章有错误的地方请大家多多包涵,不吝在评论区指正哦~ 声明:本文使用JDK1.8 一、线程池简介 线程池可以看做是线程的集合。在没有任务时线程处于空
Java3y
2018/06/11
8050
【死磕Java并发】-----J.U.C之线程池:线程池的基础架构
原文出处http://cmsblogs.com/ 『chenssy』 经历了Java内存模型、JUC基础之AQS、CAS、Lock、并发工具类、并发容器、阻塞队列、atomic类后,我们开始JUC的最
用户1655470
2018/04/26
6520
【死磕Java并发】-----J.U.C之线程池:线程池的基础架构
Java线程池了解一下?
其实常用Java线程池本质上都是由ThreadPoolExecutor或者ForkJoinPool生成的,只是其根据构造函数传入不同的实参来实例化相应线程池而已。
技术从心
2019/08/07
4200
线程池详解(通俗易懂超级好)「建议收藏」
【理解】线程池基本概念 【理解】线程池工作原理 【掌握】自定义线程池 【应用】java内置线程池 【应用】使用java内置线程池完成综合案例
全栈程序员站长
2022/11/01
5040
线程池详解(通俗易懂超级好)「建议收藏」
重温JAVA线程池精髓:Executor、ExecutorService及Executors的源码剖析与应用指南
在Java并发编程中,线程池是一个非常重要的概念。它可以帮助我们更好地管理和控制线程的使用,避免因为大量线程的创建和销毁带来的性能开销。Java的java.util.concurrent(简称JUC)包中提供了一套丰富的线程池工具,包括Executor接口、ExecutorService接口以及Executors工厂类等。本文将详细介绍这些工具的使用和原理,帮助大家更好地理解和应用Java中的线程池技术。
公众号:码到三十五
2024/03/19
2.4K0
Java 并发工具包-常用线程池
java.util.concurrent.ExecutorService 接口表示一个异步执行机制,使我们能够在后台执行任务。因此一个 ExecutorService 很类似于一个线程池。实际上,存在于 java.util.concurrent 包里的 ExecutorService 实现就是一个线程池实现。
高广超
2018/12/12
1.1K0
Java多线程之细说线程池
前言   在认识线程池之前,我们需要使用线程就去创建一个线程,但是我们会发现有一个问题:    如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。   那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?   在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPoolExecutor类中的方法讲起,然后
用户1195962
2018/01/18
1.7K0
Java多线程之细说线程池
Java的Executor框架和线程池实现原理
Executor接口是Executor框架中最基础的部分,定义了一个用于执行Runnable的execute方法,它没有实现类只有另一个重要的子接口ExecutorService
全栈程序员站长
2022/11/17
4680
Java的Executor框架和线程池实现原理
【JUC基础】13. 线程池(二)
Executors类是Java并发工具包(java.util.concurrent)中提供的一个工具类,用于创建和管理线程池。它提供了一些静态方法,用于创建不同类型的线程池,简化了线程池的创建和配置过程。
有一只柴犬
2024/01/25
2050
【JUC基础】13. 线程池(二)
Java并发编程笔记——J.U.C之executors框架:executors框架设计理念
juc-executors框架是整个J.U.C包中类/接口关系最复杂的框架,真正理解executors框架的前提是理清楚各个模块之间的关系,高屋建瓴,从整体到局部才能透彻理解其中各个模块的功能和背后的设计思路。
须臾之余
2019/07/26
5700
Java并发编程笔记——J.U.C之executors框架:executors框架设计理念
重学 Java 线程基础之线程池
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。
啵啵肠
2023/11/28
2130
java多线程系列:Executors框架
Executor是一个接口,里面提供了一个execute方法,该方法接收一个Runable参数,如下
云枭
2019/02/28
5910
Java多线程学习(八)线程池与Executor 框架
Java面试通关手册(Java学习指南,欢迎Star,会一直完善下去,欢迎建议和指导):https://github.com/Snailclimb/Java_Guide
用户2164320
2018/07/08
1.1K0
Java多线程学习(八)线程池与Executor 框架
深入理解 Java 线程池
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。
静默虚空
2019/12/26
5000
深入理解 Java 线程池
21.3 Java 线程池
线程是在一个进程中可以执行一系列指令的执行环境,或称运行程序。多线程编程指的是用多个线程并行执行多个任务。当然,JVM 对多线程有良好的支持。
acc8226
2022/05/17
3630
21.3 Java 线程池
面试-线程池的成长之路
线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。
猿天地
2018/07/25
6390
Java线程池分析
在执行一个异步任务或并发任务时,往往是通过直接new Thread()方法来创建新的线程,这样做弊端较多,更好的解决方案是合理地利用线程池,线程池的优势很明显,如下:
九州暮云
2019/08/21
4470
Java线程池分析
相关推荐
线程池你真不来了解一下吗?
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验