Java线程池必知必会知识点总结

目录
  • 1、线程数使用开发规约
  • 2、ThreadPoolExecutor源码
    • 1.构造函数
    • 2.核心参数
    • 3.execute()方法
  • 3、线程池的工作流程
  • 4、Executors创建返回ThreadPoolExecutor对象(不推荐)
    • 1.Executors#newCachedThreadPool=>创建可缓存的线程池
    • 2.Executors#newSingleThreadExecutor=>创建单线程的线程池
    • 3.Executors#newFixedThreadPool=>创建固定长度的线程池
  • 5、线程池的合理配置
  • 6、拒绝策略
  • 7、线程池的五种运行状态
  • 总结

1、线程数使用开发规约

阿里巴巴开发手册中关于线程和线程池的使用有如下三条强制规约

【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeatureOfGroup

public class UserThreadFactory implements ThreadFactory {
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
/**
* 定义线程组名称,在利用 jstack 来排查问题时,非常有帮助
*/

UserThreadFactory(String whatFeatureOfGroup) {
namePrefix = "From UserThreadFactory's " + whatFeatureOfGroup + "-Worker-";
}

@Override
public Thread newThread(Runnable task) {
String name = namePrefix + nextId.getAndIncrement();
Thread thread = new Thread(null, task, name, 0);
System.out.println(thread.getName());
return thread;
}
}

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。

如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这

样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

1) FixedThreadPool 和 SingleThreadPool:

允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2) CachedThreadPool:

允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

2、 ThreadPoolExecutor源码

1. 构造函数

UML图:

ThreadPoolExecutor的构造函数共有四个,但最终调用的都是同一个:

2.核心参数

  • corePoolSize => 线程池核心线程数量
  • maximumPoolSize => 线程池最大数量
  • keepAliveTime => 线程池的工作线程空闲后,保持存活的时间。如果任务多而且任务的执行时间比较短,可以调大keepAliveTime,提高线程的利用率。
  • unit => 时间单位
  • workQueue => 线程池所使用的缓冲队列,队列类型有:
    • ArrayBlockingQueue,基于数组结构的有界阻塞队列,按FIFO(先进先出)原则对任务进行排序。使用该队列,线程池中能创建的最大线程数为maximumPoolSize
    • LinkedBlockingQueue,基于链表结构的无界阻塞队列,按FIFO(先进先出)原则对任务进行排序,吞吐量高于ArrayBlockingQueue。使用该队列,线程池中能创建的最大线程数为corePoolSize。静态工厂方法 Executor.newFixedThreadPool()使用了这个队列。
    • SynchronousQueue,一个不存储元素的阻塞队列。添加任务的操作必须等到另一个线程的移除操作,否则添加操作一直处于阻塞状态。静态工厂方法 Executor.newCachedThreadPool()使用了这个队列。
    • PriorityBlokingQueue:一个支持优先级的无界阻塞队列。使用该队列,线程池中能创建的最大线程数为corePoolSize。
  • threadFactory => 线程池创建线程使用的工厂
  • handler => 线程池对拒绝任务的处理策略,主要有4种类型的拒绝策略:
    • AbortPolicy:无法处理新任务时,直接抛出异常,这是默认策略。
    • CallerRunsPolicy:用调用者所在的线程来执行任务。
    • DiscardOldestPolicy:丢弃阻塞队列中最靠前的一个任务,并执行当前任务。
    • DiscardPolicy:直接丢弃任务。

3.execute()方法

  • 如果当前运行的线程少于corePoolSize,则创建新的工作线程来执行任务(执行这一步骤需要获取全局锁)。
  • 如果当前运行的线程大于或等于corePoolSize,而且BlockingQueue未满,则将任务加入到BlockingQueue中。
  • 如果BlockingQueue已满,而且当前运行的线程小于maximumPoolSize,则创建新的工作线程来执行任务(执行这一步骤需要获取全局锁)。
  • 如果当前运行的线程大于或等于maximumPoolSize,任务将被拒绝,并调用RejectExecutionHandler.rejectExecution()方法。即调用饱和策略对任务进行处理。

3、线程池的工作流程

执行逻辑说明:

  • 判断核心线程数是否已满,核心线程数大小和corePoolSize参数有关,未满则创建线程执行任务
  • 若核心线程池已满,判断队列是否满,队列是否满和workQueue参数有关,若未满则加入队列中
  • 若队列已满,判断线程池是否已满,线程池是否已满和maximumPoolSize参数有关,若未满创建线程执行任务
  • 若线程池已满,则采用拒绝策略处理无法执执行的任务,拒绝策略和handler参数有关

4、Executors创建返回ThreadPoolExecutor对象(不推荐)

Executors创建返回ThreadPoolExecutor对象的方法共有三种:

1. Executors#newCachedThreadPool => 创建可缓存的线程池

  • corePoolSize => 0,核心线程池的数量为0
  • maximumPoolSize => Integer.MAX_VALUE,可以认为最大线程数是无限的
  • keepAliveTime => 60L
  • unit => 秒
  • workQueue => SynchronousQueue

弊端:maximumPoolSize => Integer.MAX_VALUE可能会导致OOM

2. Executors#newSingleThreadExecutor => 创建单线程的线程池

SingleThreadExecutor是单线程线程池,只有一个核心线程:

  • corePoolSize => 1,核心线程池的数量为1
  • maximumPoolSize => 1,只可以创建一个非核心线程
  • keepAliveTime => 0L
  • unit => 毫秒
  • workQueue => LinkedBlockingQueue

弊端:LinkedBlockingQueue是长度为Integer.MAX_VALUE的队列,可以认为是无界队列,因此往队列中可以插入无限多的任务,在资源有限的时候容易引起OOM异常

3. Executors#newFixedThreadPool => 创建固定长度的线程池

  • corePoolSize => 1,核心线程池的数量为1
  • maximumPoolSize => 1,只可以创建一个非核心线程
  • keepAliveTime => 0L
  • unit => 毫秒
  • workQueue => LinkedBlockingQueue

它和SingleThreadExecutor类似,唯一的区别就是核心线程数不同,并且由于使用的是LinkedBlockingQueue,在资源有限的时候容易引起OOM异常

5、线程池的合理配置

从以下几个角度分析任务的特性:

  • 任务的性质:CPU 密集型任务、IO 密集型任务和混合型任务。
  • 任务的优先级:高、中、低。
  • 任务的执行时间:长、中、短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备的 CPU 个数。

  • CPU 密集型任务:配置尽可能小的线程,如配置 cpu核心数+1 个线程的线程池。
  • IO 密集型任务 :由于线程并不是一直在执行任务,则配置尽可能多的线程,如2 ∗ Ncpu
  • 混合型任务:如果可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务。只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率;如果这两个任务执行时间相差太大,则没必要进行分解。

优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理,它可以让优先级高的任务先得到执行。但是,如果一直有高优先级的任务加入到阻塞队列中,那么低优先级的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果,线程数应该设置得较大,这样才能更好的利用 CPU。

建议使用有界队列,有界队列能增加系统的稳定性和预警能力。可以根据需要设大一点,比如几千。使用无界队列,线程池的队列就会越来越大,有可能会撑满内存,导致整个系统不可用。

处理拒绝策略有以下几种比较推荐:

在程序中捕获RejectedExecutionException异常,在捕获异常中对任务进行处理。针对默认拒绝策略使用CallerRunsPolicy拒绝策略,该策略会将任务交给调用execute的线程执行【一般为主线程】,此时主线程将在一段时间内不能提交任何任务,从而使工作线程处理正在执行的任务。此时提交的线程将被保存在TCP队列中,TCP队列满将会影响客户端,这是一种平缓的性能降低自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可如果任务不是特别重要,使用DiscardPolicy和DiscardOldestPolicy拒绝策略将任务丢弃也是可以的如果使用Executors的静态方法创建ThreadPoolExecutor对象,可以通过使用Semaphore对任务的执行进行限流也可以避免出现OOM异常。

6、拒绝策略

有以下几种比较推荐:

  • 在程序中捕获RejectedExecutionException异常,在捕获异常中对任务进行处理。针对默认拒绝策略
  • 使用CallerRunsPolicy拒绝策略,该策略会将任务交给调用execute的线程执行【一般为主线程】,此时主线程将在一段时间内不能提交任何任务,从而使工作线程处理正在执行的任务。此时提交的线程将被保存在TCP队列中,TCP队列满将会影响客户端,这是一种平缓的性能降低
  • 自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可
  • 如果任务不是特别重要,使用DiscardPolicy和DiscardOldestPolicy拒绝策略将任务丢弃也是可以的如果使用Executors的静态方法创建ThreadPoolExecutor对象,可以通过使用Semaphore对任务的执行进行限流也可以避免出现OOM异常。
  • 参考文章:8大拒绝策略

7、线程池的五种运行状态

线程状态:

不同于线程状态,线程池也有如下几种 状态:

RUNNING :该状态的线程池既能接受新提交的任务,又能处理阻塞队列中任务。

SHUTDOWN:该状态的线程池不能接收新提交的任务,但是能处理阻塞队列中的任务。(政府服务大厅不在允许群众拿号了,处理完手头的和排队的政务就下班)

处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。

注意:finalize() 方法在执行过程中也会隐式调用shutdown()方法。

STOP:该状态的线程池不接受新提交的任务,也不处理在阻塞队列中的任务,还会中断正在执行的任务。(政府服务大厅不再进行服务了,拿号、排队、以及手头工作都停止了。)

在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用shutdownNow() 方法会使线程池进入到该状态;

TIDYING:如果所有的任务都已终止,workerCount (有效线程数)=0。

线程池进入该状态后会调用 terminated() 钩子方法进入TERMINATED 状态。

TERMINATED:在terminated()钩子方法执行完后进入该状态,默认terminated()钩子方法中什么也没有做。

【参考文章】

【1】《JAVA并发编程艺术》

【2】tech.meituan.com/2020/04/02/…

总结

到此这篇关于Java线程池必知必会知识点的文章就介绍到这了,更多相关Java线程池必知必会内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java中四种线程池的使用示例详解

    在什么情况下使用线程池? 1.单个任务处理的时间比较短 2.将需处理的任务的数量大 使用线程池的好处: 1.减少在创建和销毁线程上所花的时间以及系统资源的开销 2.如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及"过度切换". 本文详细的给大家介绍了关于Java中四种线程池的使用,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: FixedThreadPool 由Executors的newFixedThreadPool方法创建.它是一种线程数量固定的线程

  • Java 线程池详解及实例代码

    线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁.如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些"池化资源"技术产生的原因. 例如Android中常见到的很多通用组件一般都离不开"池"的概念,如各种图片

  • java线程池:获取运行线程数并控制线程启动速度的方法

    在java里, 我们可以使用Executors.newFixedThreadPool 来创建线程池, 然后就可以不停的创建新任务,并用线程池来执行了. 在提交任务时,如果线程池已经被占满,任务会进到一个队列里等待执行. 这种机制在一些特定情况下会有些问题.今天我就遇到一种情况:创建线程比线程执行的速度要快的多,而且单个线程占用的内存又多,所以很快内存就爆了. 想了一个办法,就是在提交任务之前,先检查目前正在执行的线程数目,只有没把线程池占满的时候在去提交任务. 代码很简单: int thread

  • Java 线程池ExecutorService详解及实例代码

    Java 线程池ExecutorService 1.线程池 1.1什么情况下使用线程池 单个任务处理的时间比较短. 将需处理的任务的数量大. 1.2使用线程池的好处 减少在创建和销毁线程上所花的时间以及系统资源的开销. 如果不使用线程池,有可能造成系统创建大量线程而导致消耗系统内存以及"过度切换"; 2.ExecutorService和Executors 2.1简介 ExecutorService是一个接口,继承了Executor, public interface ExecutorS

  • java中通用的线程池实例代码

    复制代码 代码如下: package com.smart.frame.task.autoTask; import java.util.Collection;import java.util.Vector; /** * 任务分发器 */public class TaskManage extends Thread{    protected Vector<Runnable> tasks = new Vector<Runnable>();    protected boolean run

  • 详谈Java几种线程池类型介绍及使用方法

    一.线程池使用场景 •单个任务处理时间短 •将需处理的任务数量大 二.使用Java线程池好处 1.使用new Thread()创建线程的弊端: •每次通过new Thread()创建对象性能不佳. •线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom. •缺乏更多功能,如定时执行.定期执行.线程中断. 2.使用Java线程池的好处: •重用存在的线程,减少对象创建.消亡的开销,提升性能. •可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞

  • Java ExecutorService四种线程池使用详解

    1.引言 合理利用线程池能够带来三个好处.第一:降低资源消耗.通过重复利用已创建的线程降低线程创建和销毁造成的消耗.第二:提高响应速度.当任务到达时,任务可以不需要的等到线程创建就能立即执行.第三:提高线程的可管理性.线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控.但是要做到合理的利用线程池,必须对其原理了如指掌. 2.线程池使用 Executors提供的四种线程 1.newCachedThreadPool创建一个可缓存线程池

  • 四种Java线程池用法解析

    本文为大家分析四种Java线程池用法,供大家参考,具体内容如下 1.new Thread的弊端 执行一个异步任务你还只是如下new Thread吗? new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub } } ).start(); 那你就out太多了,new Thread的弊端如下: a. 每次new Thread新建对象性能差. b. 线程缺乏统一管理,可能无限

  • java线程池使用后到底要关闭吗

    线程池做什么 网络请求通常有两种形式: 第一种,请求不是很频繁,而且每次连接后会保持相当一段时间来读数据或者写数据,最后断开,如文件下载,网络流媒体等. 另一种形式是请求频繁,但是连接上以后读/写很少量的数据就断开连接.考虑到服务的并发问题,如果每个请求来到以后服务都为它启动一个线程,那么这对服务的资源可能会造成很大的浪费,特别是第二种情况. 因为通常情况下,创建线程是需要一定的耗时的,设这个时间为T1,而连接后读/写服务的时间为T2,当T1>>T2时,我们就应当考虑一种策略或者机制来控制,使

  • Java实现终止线程池中正在运行的定时任务

    最近项目中遇到了一个新的需求,就是实现一个可以动态添加定时任务的功能.说到这里,有人可能会说简单啊,使用quartz就好了,简单粗暴.然而quartz框架太重了,小项目根本不好操作啊.当然,也有人会说,jdk提供了timer的接口啊,完全够用啊.但是我们项目的需求完全是多线程的模型啊,而timer是单线程的,so,楼主最后还是选择了jdk的线程池. 线程池是什么 Java通过Executors提供四种线程池,分别为: newCachedThreadPool :创建一个可缓存线程池,如果线程池长度

随机推荐