最近在给小伙伴说明线程池技术的时候,用到了一个例子,发现比较适合,在这记录一下。
很多书籍和文章都会说,如果请求很多的话,频繁的创建和销毁线程容易造成资源的消耗和浪费,降低系统的响应,这个时候,需要对线程进行的统一分配、监控、管理,线程池技术就派上用场了。
先看一下Java中关于线程池 ThreadPoolExecutor 的创建
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
分别对这几个参数做一个说明:
- corePoolSize :核心线程数,线程池中可以长期存在的线程数量,即使没有任务提交到线程池,也不会被销毁。
- maximumPoolSize :最大线程数,线程池中最多可以创建来执行任务的线程上限。
- keepAliveTime :存活时间,超过 maximumPoolSize 数量外的线程的存活时间,超过这个时间没有执行任务时会被销毁。
- TimeUnit : keepAliveTime 的单位。
- workQueue : 任务队列,没有得到执行的任务,会放入这个队列中等待空闲的线程来执行它。
- threadFactory : 线程工厂,定义创建新线程的方式,包括线程名称,线程组等。
- handler :拒绝策略,当 workQueue 已满,并且 线程数量达到了maximumPoolSize,会对新提交的任务执行拒绝策略。
再来看看线程池的工作原理:
- 如果线程池中的线程数量没超过corePoolSize,创建一个新线程来执行任务。
- 如果线程池中的线程数量达到corePoolSize,将任务放入workQueue。
- 如果workQueue已满,线程池中的线程数量没超过maximumPoolSize,创建新线程来执行任务。
- 线程池中的线程执行完一个任务后,会从workQueue取任务执行。
- 超过corePoolSize的线程如果空闲超过keepAliveTime没有执行任务,就要进行销毁。
- 如果workQueue已满,且线程池中的线程数量达到maximumPoolSize,执行拒绝策略 handler。
- - - - - - - - - - - - - - 这是一个分割线 - - - - - - - - - - - - -
上边就是官网中对线程池参数的一些说明,这一个个参数看起来都什么玩意儿,说的是人话吗,好吧,那我们用生活中的例子来重新说明一下。
首先,为什么要是用线程池技术,为什么说创建和销毁线程会消耗资源
让我们想象一下,有一个旅游村(假设叫象牙村吧)住着一户人家,就叫 老王 吧(不是老王八),经常有来旅游的人来化个缘吃个饭,这时候,老王把桌子椅子摆上,让这个人坐好,然后根据这个人的需要把他要吃的饭做好,等这个人吃完饭走了之后,把桌子椅子收拾好。假设来吃饭的人不多的话还好,来一个人就临时摆个桌子椅子就行;但是如果来吃饭的人很多,每次都要先把椅子摆好,等一个人吃完,把桌椅收起来,再把桌椅搬出来给下一个人用,这样反反复复,很浪费搬桌子的时间。这个时候,老王想:来旅游人也挺多的,我干脆开个饭店得了。
线程池技术应运而生:
- 线程就相当于吃饭的桌子,每张桌子要配备一个服务员
- 饭店就相当于线程池
接着,线程池里的参数对应过来代表什么意思:
- corePoolSize :饭店大厅的桌子数,能供多少人共同吃饭,就算没人来吃饭,这个桌子也不会收起来。
- maximumPoolSize :最多桌子数,大厅坐不下了,饭店在院子里再摆新的桌子。
- keepAliveTime :存活时间,院子里的桌子多长时间没人用了,就要收起来。
- TimeUnit : keepAliveTime 的单位。
- workQueue : 饭店大厅坐满了的话,就要先排队。
- threadFactory : 生产桌子的方式,知道是哪个饭店第几桌。这个有什么用呢?如果不指定的话,假设老王开了个饭店,老李开了个饭店,他们都没有自己指定的桌子供货商,那就默认使用村子里生产的桌子,按顺序的话就是 象牙村1号桌、象牙村2号桌,万一发生了意外,客人吃着吃着肚子不舒服,或者这一桌的客人喝大了打了起来,只知道是 象牙村X号桌 出了问题,不知道该找老王还是老李,但是如果他们有自己的供货商,桌子就变成了老王饭店1号桌,老李饭店9号桌,出了问题,能够溯源。所以一般建议大家使用自己的线程工厂,按业务对线程进行命名。
- handler :当排队的队伍排满了,院子里也坐满了人,就要对新来的人执行拒绝策略。
最后,看看怎么用这个例子来解释线程池的工作原理:
- 有一个客人要来吃饭的时候,如果饭店大厅没有坐满,就在大厅摆上桌椅,让这个客人吃饭。
- 如果大厅坐满了,新来的人就要在门口的椅子上坐着排队,等待轮到自己的顺序,这个椅子可以限制数量,比如只排50个人,也可以不限制。
- 如果门口排队的椅子已经坐满了,说明今天来吃饭的人实在太多,老王就要看看还有没有多余的桌子,有的话,就在院子里把桌子摆上给新来的人吃饭。
- 一旦某一桌的客人吃完走了之后,这张桌子服务员就会,就从门口排队的客人里让最靠前的进店吃饭。
- 院子里的桌子如果一段时间没人吃饭,说明过了吃饭的高峰期,客人不那么多了,超过keepAliveTime的空闲,就要把桌子收起来,这个服务员也回去休息。
- 如果门口排队的椅子已经坐满,店里的桌子也都用完,要执行相应的拒绝策略。有这么几种策略
1. AbortPolicy :默认的,抛出一个异常。相当于用大喇叭喊了一下:“本店吃饭人数太多啦,大家别挤了”。
2. DiscardPolicy :直接丢弃。相当于跟客人说,现在人太多,不接待新客户了。
3. DiscardOldestPolicy :丢弃排队最久的任务。相当于让门口椅子上排第一个的人出去,意思是,你都排这么久了,看来你也不饿,那你就别吃了吧。
4. CallerRunsPolicy :接待者执行,意思就是在门口迎宾的那个人,把自己坐的桌子让出来给客人吃饭,但在这个客人吃完之前,他就没法再接待新的客人了。
再补充一下关于停止线程池的2个方法,这两个方法都会拒绝新任务,相当于说本店打烊了,不接待新客户,它们之间的区别在于:
- shutdown() :允许在关店之前已经来店里的客人把饭吃完。
- shutdownNow() :不但不让正在排队的客人进店吃饭,还会尝试把在店里没吃完的客人赶出去。这个方法比较暴力。