线程池有这么多细节,你都了解么

为什么要用线程池

Java中的线程池是运用场景最多的并发框架。几乎所有需要异步或者并发执行的任务都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。

第一:降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。

第二:提高效应速度。当任务产生时,可以不需要等到线程创建就立即执行。一个服务器完成一项任务所需时间为:T1创建线程时间,T2在线程中执行任务的时间,T3销毁线程时间。如果T1+T3的时间远大于T2,则可以采用线程池,以提高服务器性能。线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能。它把T1,T3分别安排在服务器程序的启动和结束时间段或者一些空闲的时间段。这样服务器程序处理客户请求时,就不会有T1,T3的开销了。

第三:控制线程数量。JVM创建线程需要的内存,不是JVM运行内存,堆内存,直接内存,而是操作系统剩余的可用内存,这个也决定了能创建的线程数,如果内存不够用,创建线程的时候便会出现内存溢出的错误。线程池可以实现对线程数量的控制,并且提供了一系列请求过载后的拒绝策略。

实战中的线程池的创建方案

DIY线程池ThreadPoolExcutor类

图片8.png

JDK为我们提供了基础的创建线程池的类ThreadPoolExcutor。

我们来看一下它的构造方法的6个参数,供我们去DIY线程池。具体的参数后续会详细讲解。

预定义线程池类Executors

JDK为我们提供了Executors类,可以只传入部分参数就实现线程池的创建。

但是底层依旧是对ThreadPoolExcutor的封装,只是方便我们快速创建具备某一种特征的线程池。

比如我们来看一个例子:

仔细看过上图之后,我们得到的结论就是,Executors类中提供的方法就是对ThreadPoolExcutor的二次封装,原理是一样的。

实际开发中,线程池创建的选择

在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行创建线程。这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面,线程的细节管理交给线程池处理,优化了资源开销。而线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式。这一方面是由于jdk中Excutor框架类虽然提供了如newFixedThreadPool(),newSingleThreadExecutor(),newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活。由于前面几种方法内部也是通过ThreadPoolExecutor方式实现的,使用ThreadPoolExecutor有助于大家明确线程池的规则,创建服务自己业务场景所需要的线程池,避免资源耗尽的风险。

下面我们就对ThreadPoolExecutor的使用方法进行一个详细的概述。

ThreadPoolExecutor创建线程池的7个参数意义(面试必考)

corePoolSize(线程池基本大小)

向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务。直到已创建的线程数等于corePoolSize。

如果当前接收的任务数已达到线程数corePoolSize,那么继续提交的任务会被保存到阻塞队列中,等待被执行。

如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。总结:线程池中线程的初始化是随着任务的创建而创建的。当任务数=corePoolSize时,线程中的线程将不再继续创建。

maximumPoolSize(线程池最大线程数)

线程池所创建的线程达到了corePoolSize,此时新传入的任务就会存入阻塞队列。当队列也满了该怎么办呢?

此时,线程池的策略是继续开辟新的临时线程,来处理超高并发场景的业务。这个临时线程数也是有上限的,算上之前开辟的corePoolSize,所有的线程总数依然要小于maximumPoolSize。当然,如果使用无界阻塞队列,此项可直接无视。

注意:这些临时生成的。为了解决高并发场景而临时生成的线程,在等业务量下降后是需要回收的。那么回收机制是怎样的呢?

keepAliveTime(线程存活保持时间)

这个参数主要是针对大于核心线程数(corePoolSize),且小于线程池最大线程数(maxmumPoolSize)的线程做处理的。这个参数规定了这些临时线程的空闲存活时间。也就是在一段时间内,你们不干活,那就毁灭吧!(为系统节约空间)。

unit(空闲线程存活时间单位)

TimeUnit.DAYS; //天

TimeUnit.HOURS; //小时

TimeUnit.MINUTES; //分钟

TimeUnit.SECONDS; //秒

TimeUnit.MILLISECONDS; //毫秒

TimeUnit.MICROSECONDS; //微妙

TimeUnit.NANOSECONDS; //纳秒

workQueue(任务队列)

用于传输和保存等待任务的阻塞队列。一般来说,我们应该尽量使用有有界队列。因此使用无界队列会对线程池带来如下影响。

1. 当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待。因此,无意外情况下线程池的线程数不会超过corePoolSize。

2. 由于1,使用无界队列时,maxmumPoolSize将是无效参数。

3. 由于1,2。使用无界队列时,keepAliveTime也将一个无效参数。

4. 更重要的,使用无界queue可能会导致资源耗尽,有界队列可反正资源耗尽。用时即使使用有界队列,也要尽量控制队列在大小合适的范围。

5. 所以,我们一般会使用,ArrayBlockingQueue、LinkedBlockingQueue、 SynchronousQueue、PriorityBlockingQueue来限制队列的大小。

threadFactory(线程工厂)

我们在使用线程池的时候,通常来说使用的默认ThreadFactory。默认的Factory会使得我们线程的name无法见名知意,也会给后续排查OOM异常时候带来一些阻碍。所以一般情况下建议使用线程池的时候,实现ThreadFactory方法,设置一个和业务相关的线程名字。

并且我们可以设置线程的一些属性,例如设置为守护线程。实际上就是对传入的线程再进行一次统一增强。

图片11.png

上面的例子中列举了一个重命名的简单方法,实际项目中不会这么用。如果需要规范命名,那就需要老老实实实现ThreadFactory接口。

说起命名,其实有一个博主有一个自己习惯的书写方法,就是使用构造方法给任务线程起名。举一个例子:

图片12.png

首先在任务中,设置一个taskName的成员变量。在通过构造函数传入。

图片13.png

然后在调用层就可以轻松地起名,与获取名字啦!

RejectedExecutionHandler

线程池的饱和策略。当阻塞队列满了,且没有空闲的工作线程。如果继续提交任务,必须采取一种策略处理该任务。线程池提供了4种策略:

AbortPolicy(拒绝并通知)

直接抛出异常,默认策略。(线程池因公司人员已满,拒绝继续吸纳人才,并且给你回馈一个消息)

CallerRunPolicy(交给主线程自己干)

用调用者所在的线程来执行任务。(主线程:“线程池,帮我把这个任务一做。” 线程池:“滚,自己干” 主线程:“得嘞!”)。

DiscardOldestPolicy(插队)

丢弃阻塞队列中最靠前的任务,并执行当前任务(主线程:“线程池,这个任务是领导的儿子” 线程池“得嘞,那个队列里马上要执行的任务,你滚吧,上头有关系户插队,我也保不住你了” 线程池中马上执行的任务:“******”)

DiscardPolicy(拒绝不通知)

直接丢弃任务。(线程池因公司人员已满,拒绝继续吸纳人才,并且也不通知你)。

当然也可以根据应用场景实现RejectedExcutionHandler接口,自定义饱和策略。如记录日志或持久化存储不能处理的任务。

扩展线程池

能扩展线程池的功能么?比如在任务执行的前后做一点我们自己的业务工作?

实际上,JDK已经为我们预留了接口。在线程池核心方法中,我们发现有着三个空的方法等待我们去重写:

beforeExecute(线程进行前执行)

图片14.png

afterExecute(线程结束时执行)

 图片15.png

terminated(线程池结束时执行)

图片16.png

使用案例

图片17.png

有点类似装饰者模式。每个任务在执行前后都会调用beforExecute和afterExcute方法。相当于执行了一个切面,对每一个线程进行了一次增强。在调用shutdown方法后则会调用terminated()方法。

线程池的工作机制

(1) 如果当前运行的线程少于corePoolSize,则创建新的线程来执行任务(注意:执行这一步骤需要获取全局锁)。

(2) 如果运行的线程等于或多于corePoolSize,则将任务加入BlockngQueue。

(3) 如果无法将任务加入BolckingQueue(队列已满)。则创建新的线程来处理任务。

(4) 如果创建新线程数超过maximumPoolSize,任务将被拒绝。并调用拒绝策略方法。

图片18.png

提交任务

execute无返回值

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池成功执行。

图片19.png

Future,FutureTask,submit有返回值

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象。通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值。get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回。这时候有可能任务没有执行完。

返回值为Future案例

图片20.png

图片21.png

返回值为FutureTask案例

FutureTask不但适用于我们的线程池环境,也适用于普通的Thread类,来获取任务的返回结果,我们来看看例子吧。

此处的Task类与上述案例一致,就不做过多的重复复制了。

使用ExecutorService线程池

图片22.png

在使用方法上,FutureTask与Future有些相似,Future偏向于对excutor的执行结果增强,FutureTask偏向于增强任务,通过增强后的任务获取返回值。具体可以仔细动手打打代码作比较。

使用Thread类

图片23.png

这样看下来,FutureTask具有更强大的兼容性。

Future和FutureTask的区别

我们从源码来简单分析:

首先是Future,我们看到Future是一个接口。

图片24.png

接着我们来看一下FutureTask:

图片26.png

图片25.png

我们看到FutureTask实现了RunableFuture,RunnableFuture再次继承了Runnable与Future接口。因此我们可以进行总结:

Future是一个接口,FutureTask是Future的一个实现类,并实现了Runnable,因此FutureTask可以传递到线程对象Thread中新建一个线程执行。所以可以通过Excutor(线程池)来执行,也可传递给Thread对象执行。
如果在主线程中需要执行比较耗时的操作,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
FutureTask是为了弥补Thread的不足而设计的,它可以让程序员准确地知道线程什么时候执行完成并获得到线程执行完成后返回的结果(如果有需要)。
FutureTask是一种可以取消的异步的计算任务。它的计算是通过Callable实现的,它等价于可以携带结果的Runnable,并且有三个状态:等待、运行和完成。完成包括所有计算以任意的方式结束,包括正常结束、取消和异常。
Executor框架利用FutureTask来完成异步任务,并可以用来进行任何潜在的耗时的计算。一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。

关闭线程池

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池的工作线程,然后逐个调用线程的interrupt方法来中断线程。所以无法响应中断的任务可能永远无法终止。但是这两者也存在区别。

  1. shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有正在执行或暂停任务的线程,返回等待执行任务的列表。
  2. shutdown只是将线程池的状态设置为shutdown状态,然后中断所有没执行的任务线程。

举一个例子:好比你正在吃包子。手里正在啃的包子数就是正在执行的任务。旁边蒸笼(任务队列)中也存在许多任务。此时,如果此时你接收到了shutdown执行,相当于把你手头上的包子啃完再停止。如果是shutdownNow,就尝试立刻停止吃手头的包子(不一定成功)。笼子里的包子两个指令都让你绝对不会再吃了。

合理配置线程池

要想合理地配置线程池,就必须首先分析任务特性。可以从一下几个角度来分析:

1. 任务的性质。CPU密集型任务,IO密集型任务和混合型任务。

2. 任务的优先级:高,中和低。

3. 任务的执行时间:长,中和短,

4. 任务的依赖性:是否依赖其他系统资源,如数据连接。

CPU密集型任务

尽量使用较小的线程池,一般为CPU核心数+1.

何为CPU密集型?就是CPU需要在一段时间内集中处理的任务。好比做一个非常复杂的运算。此时如果是以相同的CPU一气呵成的形式进行计算的效率,明显会高于多个CPU来回切换导致上下文切换的执行效率。

IO密集型任务

可以使用稍大的线程池,一般为2*CPU核心数+1.

因为IO操作主要消耗的是硬盘速度,此时对CPU计算的要求就不那么高了。那么此时我们就可以通过加大线程池中的线程数,并发的执行任务,提高CPU的使用率。

混合型任务

可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池处理。

这里牵扯到一个拆分与汇总的概念。

只要拆分后的任务执行时间相差不大,那么进行拆分的执行并汇总的效率就会高于随便使用一种线程池效率高。

但如果拆分后的任何一个类型的任务执行时间远小于或大于另一个任务,那么在任务结束后的汇总阶段,先执行完的线程池反而要去等后执行完的线程池任务。最终时间依然取决于后执行完的任务。然而这中间还要算上任务拆分与之后合并的时间,得不偿失。

依赖其他资源

如某个任务依赖数据库的连接返回的结果,这时等待的时间越长,则CPU空闲的时间越长,那么线程数量应尽量设置大。这样虽然会频繁切换任务执行,但是可以尽量避免在一个空闲任务等待过长的时间,从而更好的利用CPU。

结论

线程等待时间所占比例越高,则需要更多的线程。线程cpu时间所占比例越高,需要越少线程。

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

建议使用有界队列,有界队列可以增加系统的稳定性和预警能力。可以根据需求设置大一点,比如几千。

假设现在有一个web项目,里面使用线程池来处理任务。在某些情况下,系统里后台任务线程池的队列和线程池全满了,不断抛出任务的异常。通过排查发现是数据库出现了问题,导致sql执行变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作全部阻塞,任务积压在线程池里。

如果当我们设置成无界队列,那么线程池的队列数据就会越来越多,可能撑爆内存,导致整个系统不可用,而不只是后台任务出现问题。

预定义线程池

FixedThreadPool

图片27.png

图片28.png

创建使用固定线程数的FixedThreadPool的API。适用于需要限制当前线程数量的应用场景。一般用于负载比较重的服务器。FixedThreadPool的corePoolSize和maximumPoolSize都被设置为创建FixedThreadPool时指定的nThreads。(corePoolSize=maximumPoolSize)

当线程池中的线程数大于corePoolSize时,就直接进入LinkedBlockingQueue队列中进行等待,队列容量为Integer.MAX_VALUE,因此是无界队列。

SingleThreadExecutor

图片29.png

图片30.png

创建使用单个线程的SingleThread-Executor的API,用于需要保证顺序地执行各个任务,并且在任务时间点都不会有多个线程活动的应用场景。

corePoolSize和maximumPoolSize被设置为1。其他参数与FixedThreadPool相同。SingleThreadExecutor使用有界队列LinkedBlockingQueue作为线程池的工作队列,容量为Integer.MAX_VALUE。

CachedThreadPool

图片31.png

图片32.png

创建一个会根据需要创建新线程CachedThreadPool的API。大小无界的线程池。适用于执行很多短期异步任务的小程序,或者负载较轻的服务器。

corePoolSize被设置为0,maximumPoolSize被设置为Integer.MAX_VALUE。这里把keepAliveTime设置为60L,意味着CachedThreadPool中的空闲线程等待新任务的最长时间为60s,空闲线程超过60s后将会被终止。

FixedThreadPool和SingleThreadExecutor使用有界队列LinkedBlockingQueue作为线程池的工作队列。CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列。但CachedThreadPool的maxmumPool是无界的,这意味着,如果主线程提交任务的速度远高于线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。

WorkStealingPool

图片33.png

从上面代码的介绍,最明显的用意就是它是一个并行的线程池,参数中传入的是一个本机的CPU核心数,这里和之前就有很明显的区别,前面4种线程池都有核心线程数、最大线程数等等,而这就使用了一个并发线程数解决问题。从介绍中,还说明这个线程池不会保证任务的顺序执行,也就是 WorkStealing 的意思,抢占式的工作。底层使用forkjoin实现,可以提升业务的执行效率。

ScheduledThreadPoolExecutor

这个线程池的创建就比较特殊了。其他的线程池都是为了并行执行任务而存在,这个线程池则是为了解决并行,并且循环执行重复任务而存在的。以功能来说,非常的类似Time的使用。那么,为什么线程池要提供这个看似重复的功能呢?

假设,我们需要用到一个定时器处理蓝牙设备接收的数据,并且需要处理的频率是很快的,这就需要一个稳定的定时器来保证数据的长久进行。ScheduledThreadPoolExecutor这个类就是个很好的选择。

正常情况下,定时器我们都是用Timer和TimerTask这两个类就能完成定时任务,并且设置延长时间和循环时间间隔。

ScheduledThreadPoolExecutor也能完成Timer一样的定时任务,并且时间间隔更加准确。

缩小误差

在后台看一下Timer执行程序有可能延时1,2毫秒。如果是1s执行一次的任务,1分钟可能延时60毫秒,一小时延时3600毫秒,相当于3秒,实际用户看不出什么区别。

但是,如果我们的程序需要每40毫秒就执行一次任务,如果还是有1,2毫秒的误差,1秒钟可能就有25毫秒的误差。1秒钟就有25毫秒的误差,大概40秒就有秒的误差,十几分钟就有十几秒的误差。这对UI显示来说是延时非常严重的了。

而我用ScheduledThreadPoolExecutor来做40毫秒的间隔任务,一般十几分钟才有1秒多的误差,这个还是能接受的,也就是我们是用ScheduledThreadPoolExecutor这个类的原因。

可用子类说明

Timer,TimerTask的使用

虽然这里它不是主角,但是也记录在这里方便大家作对比学习。

图片34.png

图片35.png

可以看到实现了轮循执行任务的效果。但在我们出现了线程池ScheduledThreadPoolExecutor后,在它的精准度方面再次做了提升。

单个线程重复轮询SingleThreadScheduledExecutor

适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。我们来看下面的例子:

图片36.png

图片37.png

可以看到,我们这个任务已经能够做到重复执行了。

那你会不会接着疑惑,如果我误传了多个任务,使用这个单任务轮循执行器会是怎样的效果呢?我们一起来试一下。

图片39.png

图片40.png

通过结果可知,我们的轮询器不会报错,而是在两个任务之间交替执行。因此如果使用这个单任务轮循执行器,却误执行了n个任务,会让原本想轮询执行的任务效率下降n倍。此处注意一下即可。

多个线程重复轮询ScheduledThreadPoolExecutor

构造线程数等于任务数

ScheduledThreadPoolExecutor适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程数量的应用场景。我们来看一下使用场景:

图片41.png

图片42.png

我们创建线程池时,提供了两个线程。此时再创建两个任务,来看一下执行结果:

可以看到我们的任务已经可以并行执行了。

构造线程数小于任务数

那么如果我们设置的线程数比任务数少的话,会出现怎样的情况呢?

图片43.png

图片44.png

此时的任务再次变回交替执行。

构造线程数大于任务数

图片45.png

图片46.png

结果依旧是并行执行任务。

总结

因此,在使用我们的线程池定时轮询功能时,要保证我们的任务与任务之间可以按我们设置的时间去轮循执行的话,那么就要保证线程池的线程数>=执行任务数。

提交定时任务的不同方法

简单介绍一下用法:

提交一个延时 Runnable 任务(仅执行一次)

图片47.png

图片48.png

提交一个延时的 Callable 任务(仅执行一次)

提交一个固定时间间隔执行的任务(scheduleAtFixedRate)

图片49.png

scheduleAtFixedRate中,若任务处理时长超出设置的定时频率时长,本次任务执行完才开始下次任务,下次任务已经处于超时状态,会马上开始执行。

若任务处理时长小于定时频率时长,任务执行完后,定时器等待,下次任务会在定时器等待频率时长后执行。

如下例子:

设置定时任务每60s执行一次,那么从理论上应该第一次任务在第0s开始,第二次任务在第60s开始,第三次任务在120s开始,但实际第一个任务时长80s,第二次任务时长30s,第三次任务时长50s,则实际运行结构为:

第一次任务第0s开始,第80s结束。

第二次任务第80,第110s结束(上次任务已超时,本次不会再等待60s,会马上开始)

第三次任务第120s开始,第170s结束。

第四次任务第180s开始......

提交一个固定延时间隔执行的任务(scheduleWithFixedDelay)

图片50.png

这也就是我们最常见的一种写法。每次任务的起始,都会根据上一次任务结束后开始计算。但这样执行,对整个任务的总耗时无法进行预估,因为每个任务的时间是不定的。因此想要能对总耗时有一个基本把握的话,建议使用scheduleAtFixedRate。

CompletionService(返回值顺序)

其实当时在学习线程池的时候,就一直存在一个疑:

就是我的线程池每一次可以并行执行一大堆线程,但是每次给我的返回值却只有一个。那么,现在如果要我们自己来实现对结果的统一获取,该怎么实现呢?

首先,先来一个常规思路的例子。首先,我们在线程池中批量生成任务,将线程的返回数据存到一个阻塞队列中。对返回的结果进行依次遍历打印,具体实现如下:

图片51.png

图片52.png

在我们执行打印结果的时候,我们发现长任务在get结果时,会阻塞住后续短任务的返回结果。我们再次仔细分析一下问题原因。

事实上,从运行结果上来看,我们的程序好像是串行执行的。其实我们的线程池依旧是并行执行任务的(可以观察总执行时间与最长任务的耗时基本吻合)。只是当在遍历结果的时候,我们的for循环遍历是对任务进行串行化执行的。这也导致了长任务在get方法时,会阻塞我们其他完成较快的线程结果,不能及时返回,只能暂存在阻塞队列中。即时完成了任务也无法即时反馈。

这样做显然是不友好的。假如有一个很长很长的任务,一直无法返回结果,就会导致许多执行完毕的任务结果缓存在阻塞队列中,无法及时获取。针对这类问题,就到了我们的CompletionService类闪亮登场了。

CompletionService将先执行完的任务提前返回结果,不会因为之前的串行执行导致后面较快完成的任务阻塞。

下面举一个CompletionService使用的例子:

图片53.png

图片54.png

从这个例子中可以看出,我们的结果已经可以顺序返回了,短任务不会再受到长任务的影响而阻塞了。

使用CompletionService可以按照任务的完成时间来拿到任务的执行结果。它内部维护了一个阻塞队列只有任务执行完才会放进去,从而实现任务结果的有序输出。

线程池的实现

成员介绍

图片55.png

首先,我们来构造一个线程池的成员变量。这里我们提供四个参数

  1. WORK_COUNT:默认线程池中的活跃线程数。(如work_number不指定,就是这个默认值)
  2. work_number:实际线程池中的活跃线程数。
  3. taskQueue:任务队列。

图片64.png

  1. workThreads:一个线程数组,这个数组的长度会根据WORK_COUNT或work_number来创建。

构造方法

图片56.png

步骤类中的注释仔细标注。此时,我们获得了一个长度为task_count的ArrayBlockingQueue有界阻塞队列,并且创建了work_number个长期被线程池持有的线程。我们来看看每一个线程是如何实现重复使用的。

工作线程的复用实现原理

图片57.png

此时线程复用的迷雾已经逐渐解开。可能我们幻想的归还线程是多么复杂,高大上的逻辑。实际上,就是每个线程不断获取队列中的任务,如果获取不到就阻塞,直到获取成功,则继续执行。从而达到线程复用的目的。

线程池执行任务的excute方法如何实现

图片58.png

这下执行任务,也简单了,直接给队列中添加任务即可。配合上线程复用模块可知,这几个固定的线程就像永动机一般,一直在循环处理任务,只要队列里有任务就楞怂干!

销毁线程池

销毁线程池需要我们依次去停止每一个线程的工作,然后对所有数据进行清理回收,防止内存泄漏。我们来看看怎么实现吧。

图片59.png

具体每个线程的stopWorker方法做了什么呢?

图片63.png

图片60.png

原来此处我们使用的是中断线程interrupt来实现的。我们再回顾一下线程复用模块:

原来是我们每一次线程执行完毕后,执行下一个任务之前会判断是否需要结束线程。这样可以保证我们在结束线程池时,每一个正在执行中的线程都安全执行完毕后再退出线程。

到这里,整个线程池就已经实现完毕了。我们再简单看一下任务类与调用层的运用。

任务类

图片61.png

主要就是一些时间延时与打印。我们可以自己添加自己的业务逻辑。

调用层

图片62.png

是不是和我们线程池的使用方式很像呢?

注:这里我们没有对线程池的扩展线程,扩展时间以及线程池的拒绝策略进行实现,主要带大家理解线程池的线程复用的实现原理,以便大家更加深刻的理解线程池。

猜你喜欢

转载自blog.csdn.net/weixin_47184173/article/details/115195026