专栏首页一猿小讲彻底搞懂 Java 线程池,干啥都不再发憷

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

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

1

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

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

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

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

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

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

2

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

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

new Thread(new Runnable() {
    @Override    
    public void run() {  
          System.out.println("我是一个孤独的线程");        
          // do something    
    }
}).start();

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

new Thread(() -> System.out.println("我是一个孤独的线程")).start();

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

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

// 待通知的消息
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();
}

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

java.lang.OutOfMemoryError: unable to create new native thread

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

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

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

3

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

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

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

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

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 个人同时干活就行),代码改造如下。

// 待通知的消息
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 方法关闭线程池,代码如下。

// 待通知的消息
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 方法来关闭线程池,代码如下。

//4. 调用 ExcutorService 对象的 shutdownNow 方法来关闭线程池。
List<Runnable> tasks = executorService.shutdownNow();
System.out.println(tasks);

输出截图:

结论:

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

5

寄语写最后

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

本文分享自微信公众号 - 一猿小讲(yiyuanxiaojiangV5),作者:一猿小讲

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-07-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 程序员进阶系列:你真的懂 HelloWorld 吗?

    作为入了门的 Java 程序员,相信在脑海中都能够秒写出 HelloWorld.java,都知道编译成 HelloWorld.class,然后就可以跨平台执行了...

    一猿小讲
  • Executors功能如此强大,ThreadPoolExecutor功不可没(一)

    在 JDK1.5 以前,研发人员在面对线程频繁调度的场景,必须手动打造线程池,来节约系统开销(画外音:真是吃了不少苦头)。

    一猿小讲
  • Java线程池深度揭秘

    Executor 是一个接口(主要用于定义规范),定义了 execute 方法,用于接收 Runnable 对象。

    一猿小讲
  • Java多线程:还不懂线程池吗?一文带你彻底搞懂!

    总体来说,线程池有如下的优势: (1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 (2)提高响应速度。当任务到达时,任务可以不需要...

    Java小朔哥
  • 老技术新谈,Java应用监控利器JMX(2)

    上期由于架不住来自于程序员内心的灵魂的拷问,于是我们潜心修炼,与 Java 应用监控利器 JMX 正式打了个照面。

    一猿小讲
  • 最新 Java 核心技术教程,都在这了!

    以下是Java技术栈微信公众号发布的所有关于 Java 的技术干货,2021最新更新版,本文会长期更新。

    Java技术栈
  • 2019 最新 Java 核心技术教程,都在这了!

    以下是Java技术栈微信公众号发布的所有关于 Java 的技术干货,会从以下几个方面汇总,本文会长期更新。

    Java技术栈
  • 还在为怎么学习Android苦恼?看完学会这些大牛资料,2年高级3年资深不是问题!

    编程这条路能走多远,能走多久,就看一点:你学不学的明白。想学明白,就得看你会不会学习,所以编程能干多久,你值多少钱,最终看你会不会学习。

    Android技术干货分享
  • 「冰河技术」部分精华文章目录汇总

    个人研发的在高并发场景下,提供的简单、稳定、可扩展的延迟消息队列框架,具有精准的定时任务和延迟队列处理功能。自开源半年多以来,已成功为十几家中小型企业提供了精准...

    冰河
  • 聊聊怎样学习Binder

    Binder可以说是整个Android框架最重要的一个基础。如果不能吃透Binder,就谈不上对Android有多么深刻的理解。这个道理相信大部分Android...

    HowHardCanItBe
  • 关于JDK源码:我想聊聊如何更高效地阅读

    一,JDK源码是其它所有源码的基础,看懂了JDK源码再看其它的源码会达到事半功倍的效果。

    彤哥
  • 并发的核心:CAS 是什么?Java8是如何优化 CAS 的?

    大家可能都听说说 Java 中的并发包,如果想要读懂 Java 中的并发包,其核心就是要先读懂 CAS 机制,因为 CAS 可以说是并发包的底层实现原理。

    帅地
  • 并发的核心:CAS 是什么?Java8是如何优化 CAS 的?

    大家可能都听说说 Java 中的并发包,如果想要读懂 Java 中的并发包,其核心就是要先读懂 CAS 机制,因为 CAS 可以说是并发包的底层实现原理。

    猿天地
  • 如何应对面试官 “刨根问底式” 的提问?

    你面试的时候,有没有遇到过这种情况:面试官抓着一个点,变着花样的问到你答不出来。你以为他是在对你进行压力面试么,其实并不是。

    猿天地
  • 教妹学 Java 第 35 讲:intern

    “哥,你发给我的那篇文章我看了,结果直接把我给看得不想学 Java 了!”三妹气冲冲地说。

    沉默王二
  • 南京渣硕求职路(网易美团头条百度面经)+Java学习路线(拙见)

    首先自我介绍一下,楼主南京渣硕一枚,秋招主要投递JAVA后台岗位,面过以下公司:网易+美团+头条+百度+华为+中兴,拿下了网易和中兴提前批offer,华为依旧泡...

    Java架构技术
  • C语言,能开发什么?怎么去学习?

    看看很多招聘网站有关找纯粹的C语言开发的比例真的不是很多,都被Java,php,python等等语言刷屏。这对于初学正在学习C语言的小白简直就是惊天霹雳,学了没...

    诸葛青云
  • 长连接网关技术专题(五):喜马拉雅自研亿级API网关技术实践

    网关是一个比较成熟的产品,基本上各大互联网公司都会有网关这个中间件,来解决一些公有业务的上浮,而且能快速的更新迭代。如果没有网关,要更新一个公有特性,就要推动所...

    JackJiang
  • StringBuffer 和 StringBuilder 的 3 个区别!

    这么简单的一道题,栈长在最近的面试过程中,却经常遇到很多求职者说反,搞不清使用场景的情况。

    Java技术栈

扫码关注云+社区

领取腾讯云代金券