Java并发-线程池篇-附场景分析

个人博客:javalover.cc

前言

前面我们在创建线程时,都是直接new Thread();

这样短期来看是没有问题的,但是一旦业务量增长,线程数过多,就有可能导致内存异常OOM,CPU爆满等问题

幸运的是,Java里面有线程池的概念,而线程池的核心框架,就是我们今天的主题,Executor

接下来,就让我们一起畅游在Java线程池的海洋中吧

本节会用银行办业务的场景来对比介绍线程池的核心概念,这样理解起来会很轻松

简介

Executor是线程池的核心框架;

和它相对应的有一个辅助工厂类Executors,这个类提供了许多工厂方法,用来创建各种各样的线程池,下面我们先看下几种常见的线程池

// 容量固定的线程池 Executor fixedThreadPool = Executors.newFixedThreadPool(5); // 容量动态增减的线程池 Executor cachedThreadPool = Executors.newCachedThreadPool(); // 单个线程的线程池 Executor singleThreadExecutor = Executors.newSingleThreadExecutor(); // 基于调度机制的线程池(不同于上面的线程池,这个池创建的任务不会立马执行,而是定期或者延时执行) Executor scheduledThreadPool = Executors.newScheduledThreadPool(5);

上面这些线程池的区别主要就是线程数量的不同以及任务执行的时机

下面让我们开始吧

文章如果有问题,欢迎大家批评指正,在此谢过啦

目录

线程池的底层类ThreadPoolExecutor

为啥阿里不建议使用 Executors来创建线程池?

线程池的生命周期 ExecutorService

正文 1. 线程池的底层类 ThreadPoolExecutor

在文章开头创建的几个线程池,内部都是有调用ThreadPoolExecutor这个类的,如下所示

public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }

这个类是Exexutor的一个实现类,关系图如下所示:

image-20210518153445195

其中Executors就是上面介绍的辅助工厂类,用来创建各种线程池

接口ExecutorService是Executor的一个子接口,它对Executor进行了扩展,原有的Executor只能执行任务,而ExecutorService还可以管理线程池的生命周期(下面会介绍)

所以我们先来介绍下这个底层类,它的完整构造参数如下所示:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {

在介绍这些参数之前,我们可以先举个生活中的例子-去银行办业务;然后对比着来理解,会比较清晰

image

(图中绿色的窗口表示一直开着)

corePoolSize: 核心线程数,就是一直存在的线程(不管用不用);=》窗口的1号窗和2号窗

maximumPoolSize:最大线程数,就是最多可以创建多少个线程;=》窗口的1,2,3,4号窗

keepAliveTime:多余的线程(最大线程数 减去 核心线程数)空闲时存活的时间;=》窗口的3号窗和4号窗空闲的时间,如果超过keepAliveTime,还没有人来办业务,那么就会暂时关闭3号窗和4号窗

workQueue: 工作队列,当核心线程数都在执行任务时,再进来的任务就会添加到工作队列中;=》椅子,客户等待区

threadFactory:线程工厂,用来创建初始的核心线程,下面会有介绍;

handler:拒绝策略,当所有线程都在执行任务,且工作队列也满时,再进来的任务就会被执行拒绝策略(比如丢弃);=》左下角的那个小人

基本的工作流程如下所示:

image-20210518164807186

上面的参数我们着重介绍下工作队列和拒绝策略,线程工厂下面再介绍

工作队列:

ArrayBlockingQueue:

数组阻塞队列,这个队列是一个有界队列,遵循FIFO,尾部插入,头部获取

初始化时需指定队列的容量 capacity

类比到上面的场景,就是椅子的数量为初始容量capacity

LinkedBlockingQueue:

链表阻塞队列,这是一个无界队列,遵循FIFO,尾部插入,头部获取

初始化时可不指定容量,此时默认的容量为Integer.MAX_VALUE,基本上相当于无界了,此时队列可一直插入(如果处理任务的速度小于插入的速度,时间长了就有可能导致OOM)

类比到上面的场景,就是椅子的数量为Integer.MAX_VALUE

SynchronousQueue:

同步队列,阻塞队列的特殊版,即没有容量的阻塞队列,随进随出,不做停留

类比到上面的场景,就是椅子的数量为0,来一个人就去柜台办理,如果柜台满了,就拒绝

PriorityBlockingQueue

优先级阻塞队列,这是一个无界队列,不遵循FIFO,而是根据任务自身的优先级顺序来执行

初始化可不指定容量,默认11(既然有容量,怎么还是无界的呢?因为它添加元素时会进行扩容)

类比到上面的场景,就是新来的可以插队办理业务,好比各种会员

拒绝策略:

AbortPolicy(默认):

中断策略,抛出异常 RejectedExecutionException;

如果线程数达到最大,且工作队列也满,此时再进来任务,则抛出 RejectedExecutionException(系统会停止运行,但是不会退出)

DiscardPolicy:

丢弃策略,丢掉新来的任务

如果线程数达到最大,且工作队列也满,此时再进来任务,则直接丢掉(看任务的重要程度,不重要的任务可以用这个策略)

DiscardOldestPolicy:

丢弃最旧策略,丢掉最先进入队列的任务(有点残忍了),然后再次执行插入操作

如果线程数达到最大,且工作队列也满,此时再进来任务,则直接丢掉队列头部的任务,并再次插入任务

CallerRunsPolicy:

回去执行策略,让新来的任务返回到调用它的线程中去执行(比如main线程调用了executors.execute(task),那么就会将task返回到main线程中去执行)

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzwsfd.html