前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >笔记09 - 线程池刨根问底

笔记09 - 线程池刨根问底

作者头像
码农帮派
发布2021-01-12 14:55:59
3230
发布2021-01-12 14:55:59
举报
文章被收录于专栏:码农帮派码农帮派

我们知道CPU运行的最小单位是线程,Java中实现并发是通过多线程来完成的,利用多线程提高了对CPU资源的利用率,但是线程的创建和销毁是很消耗性能的。线程的创建伴随着虚拟机栈、本地方法栈、程序计数器等线程私有内存空间的创建,在线程销毁的时候也伴随着这些私有内存的回收,频繁的创建和销毁线程会占用大量的系统资源,而对线程的复用则可以有效的管理和协调线程的工作。

线程池主要解决的两个问题:

  • 1. 在执行大量异步任务的时候线程池能够提供很好的性能;
  • 2. 线程池能够提供一种资源限制和管理的手段,比如限制同时运行的线程的数量。

线程池的体系

Executor:线程池最顶端的接口,在Executor中只有一个execute方法,用于执行任务。线程的创建、调度都是由子类实现的;

ExecutorService:继承自Executor,在Executor内部实现了任务提交机制以及线程池关闭的方法;

ThreadPoolExecutor:ExecutorService的默认实现,线程池的机制大部分封装在这个类中;

ScheduledExecutorService:继承自ExecutorService,增加了定时任务;

ScheduledThreadPoolExecutor:继承自ThreadPoolExecutor,并实现了ScheduledExecutorService接口,提供定时任务处理;

ForkJoinPool:一种任务分解的线程池,需要配合ForkJoinTask来使用。

线程池的创建

JDK中为开发者提供了一个线程池的工厂类 - Executors,Executors中提供了多个静态方法,用来创建不同配置的线程池。

  • newSingleThreadExecutor

创建一个单线程化的线程池,它里面只有一个工作线程,所有提交的异步任务都会放到一个先进先出的队列中按顺序执行。

所有的异步任务都使用同一个线程执行,且异步任务执行的顺序就是异步任务被提交到线程池中的顺序:

  • newCachedThreadPool

创建一个可缓存线程的线程池,当线程池中线程的长度超过了需要的数量,则会回收多余的线程,若没有可用线程,就会创建新的线程。

上面的代码中一次性提交了5个异步任务,每个异步任务内部耗时操作500ms,此时线程池就会不断创建新的线程来执行异步任务:

下面修改一下代码,每次向线程池中提交异步任务的时候等待1000ms:

再次执行代码,可以看到所有的异步任务会按顺序的在同一个线程中被执行,这是因为单个异步任务执行完毕之后,线程池并不会立即销毁线程,而是将线程缓存起来,等待下一个异步任务的提交,继续使用已经创建的线程来执行后面的异步任务,这样极大的减少了系统因为线程创建和销毁的损耗。

  • newFixedThreadPool

创建一个线程数量固定、可重用的线程池。在使用newFixedThreadPool创建线程池的时候需要指定线程池中常驻线程的数量,在后续异步任务提交的时候,不管提交了多少个Runnable任务,线程池中始终只有指定数量的线程处理任务。

上面的代码中向newFixedThreadPool中一次性提交了10个任务,但是任务只会被3个线程分配执行。

newScheduledThreadPool

会创建一个可以处理定时任务的线程池,支持定时任务和周期性任务的执行。

上面的代码中创建了一个线程数量为2的定时线程池,通过scheduleAtFixedRate指定每隔500毫秒执行一次任务,并在5秒之后通过shutdown关闭定时任务。

线程池工作原理

线程池的机构:

上图是一个线程池的结构,在一个线程池中包含以下几部分:

  • worker集合:使用HashSet保存所有核心线程和非核心线程;
  • 等待队列:当核心核心线程的数量达到corePoolSize的时候,新提交的任务会被保存到等待队列中,等待队列是一个阻塞队列BlockingQueue;
  • ctl:是一个AtomicInterger类型的数据,二进制高3位保存线程池的状态,低29位保存线程池中线程的数量;

线程池的构造方法

构造函数的参数说明:

  • corePoolSize:表示核心线程的数量;
  • maximumPoolSize:表示线程池最大可容纳同时执行的线程数量,必须大于等于1。如果maximumPoolSize等于corePoolSize,则为固定大小的线程池;
  • keepAliveTime:表示线程池中线程空闲的时长,当线程空闲的时长达到这个值的时候,线程就会被销毁,直到线程池中剩余corePoolSize个线程;
  • unit:用来指定keepAliveTime的时间单位;
  • workQueue:等待队列,BlockingQueue类型,当请求数超过corePoolSize,任务会被首先缓存到等待队列中;
  • threadFactory:线程工厂,线程池中使用它来创建线程;
  • handler:执行拒绝策略的对象,当workQueue中的任务塞满,并且活动线程数量超过了maximumPoolSize的时候,线程池会通过该策略处理新的任务;

流程解析:

当我们通过execute或者submit向线程池提交任务,线程池在接收到任务之后,有以下几种情况:

  • 1. 当前线程池中运行的线程数量还没有达到corePoolSize数量,线程池会创建一个新的线程来执行当前提交的任务,无论之前创建的线程是否处于空闲状态;
  • 2. 当前线程池中运行的线程数量已经达到了corePoolSize数量,线程池会将新加入的任务放到等待队列中,直到某一个线程空闲了,线程池会根据等待队列中设置的优先级规则,取出一个任务执行;
  • 3. 要是线程池中运行的线程数量大于corePoolSize,并且等待队列已满,但运行线程数量并没有达到最大线程数maximumPoolSize,此时线程池会创建新的线程来执行任务;
  • 4. 要是提交的任务无法被核心线程处理,等待队列也已经塞满,有无法被非核心线程处理,线程池就会按照拒绝处理器(handler)来处理这个任务。如果没有为线程池定义RejectExecutionHandler,那么线程池就会抛出一个RejectExecutionException异常。

JDK中定义了4种保护策略:

禁止使用Executors

我们在使用多线程的时候,一般禁止使用线程池的工厂类进行线程池的创建,尤其是newFixedThreadPool和newCachedThreadPool这两个方法。

我们查看newFixedThreadPool方法和newSingleThreadExecutor方法创建线程池的过程:

可以看到线程池中传入了一个无界的阻塞队列,理论上可以添加无限个任务到线程池中,当我们向等待队列中塞入特别多的任务的时候,由于等待队列无限长,线程池永远不会执行任务丢弃的保护逻辑,就会导致OOM发生。

我们在看一下newCachedThreadPool的实现代码:

newCachedThreadPool在创建线程池的时候,声明的maximumPoolSize为Interger的最大值,当核心线程耗时很久的时候,线程池就会尝试创建新的线程执行任务,当内存中无法承受新线程的创建时,就会导致OOM发生。

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

本文分享自 码农帮派 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档