线程池是日常开发中罕用的技术,应用也非常简单,不过想应用好线程池也不是件容易的事,开发者须要一直摸索底层的实现原理,能力在不同的场景中抉择适合的策略,最大水平施展线程池的作用以及防止踩坑。

一、线程池工作流程

以下是Java线程池的工作流程,波及创立线程的参数及回绝策略,如果读者对这部分内容不太理解,可参考其余的文档,本文不在赘述。

二、线程池进阶

1、线程池的创立

须要手动通过ThreadPoolExecutor创立,使用者要十分明确业务场景并定制线程池,防止误用可能导致的问题。

以下是阿里巴巴Java开发手册中的形容:

ThreadFactory:举荐应用guava中的ThreadFactoryBuilder创立:

new ThreadFactoryBuilder().setNameFormat("name-%d").build();

2、阻塞队列在线程池中的应用

很多同学一看到阻塞队列就天然的认为出入队列都是阻塞的,应用的阻塞队列也就没必要关怀回绝策略了,其实不然,阻塞队列在工作提交和工作获取阶段应用了不同的策略。

工作提交阶段:调用的阻塞队列的offer办法,这个办法是非阻塞的,如果插入队列失败会间接返回false,并触发回绝策略;

获取工作阶段:应用的是take办法,此办法是阻塞的;

3、保障提交阶段工作不失落

有三种办法:应用CallerRunsPolicy回绝策略、自定义回绝策略、应用MQ零碎保障工作不失落。

(1)CallerRunsPolicy回绝策略

ThreadPoolExecutor.CallerRunsPolicy:由提交工作的线程解决

这种是最简略的策略,但须要留神的是如果工作耗时较长,会阻塞提交工作的线程,可能会成为零碎瓶颈。

(2)自定义回绝策略

既然Java线程默认应用的是offer提交工作,那咱们能够自定义回绝策略在工作提交失败时改为put阻塞提交。

毛病也是会阻塞提交线程,不过相比CallerRunsPolicy策略更能施展多线程的劣势。

 RejectedExecutionHandler executionHandler = (r, executor) -> {   try {     executor.getQueue().put(r);   } catch (InterruptedException e) {     Thread.currentThread().interrupt();     throw new RejectedExecutionException("Producer thread interrupted", e);   } };

(3)配合MQ保障工作不失落

应用默认的ThreadPoolExecutor.AbortPolicy策略,如果抛出RejectedExecutionException异样则返回给MQ生产失败,MQ会保障主动重试。

4、保障队列、未执行实现的工作不失落

当服务进行的时候,线程池中队列和沉闷线程中未执行实现的工作可能会造成数据失落,首先说下结论:无论采取任何策略,在Java层都不能100%保障不丢,比方机器忽然断电的状况。咱们还是能够采取肯定的措施尽量避免工作失落。

(1)线程池敞开

线程池敞开有两个办法:

shutdownNow办法:线程池回绝接管新提交的工作,同时立马敞开线程池,线程池里的工作不再执行,并抛出InterruptedException异样。

shutdown办法:线程池回绝接管新提交的工作,同时期待线程池里的工作执行结束后敞开线程池。

(2)注册敞开钩子

应用以下办法注册JVM过程敞开钩子,在钩子办法中执行线程池敞开、未解决实现的工作长久化保留等。

Runtime.getRuntime().addShutdownHook()

须要留神的是:钩子办法在应用kill -9杀死过程时不会执行,个别的杀过程的形式是先执行kill,期待一段时间,如果过程还没杀死,再执行kill -9。

要保障队列中的工作不失落,须要生产队列中的数据,发送到内部MQ中;

保障未执行实现的工作不失落,须要在抛出InterruptedException异样后,将工作参数保障到MQ中;

须要留神的是:1)尽量不要把未实现的工作保留到本地磁盘,尤其是在常常扩缩容的弹性集群里;2)捕捉InterruptedException异样后,不要做重试等耗时操作;3)须要监控工作都发送到MQ中的工夫,以便调整kill -9强制执行前的等待时间。

(3)应用MQ保障工作必须执行实现

通过下面介绍的两种形式,能够解决大部分失常进行服务丢数据的工作。不过对于极其状况下,比方断电、断网等,须要严格保障工作不失落的场景还是不能满足业务须要,这种状况下就须要依赖MQ。

计划是应用线程池的submit办法提交工作,通过future获取到工作执行实现再返回给MQ生产实现。在MQ中如何保证数据不失落是另外一个简单的话题了,这里不再深入探讨。

须要留神的是,如果采纳这种计划,须要保障解决工作的幂等性,在操作步骤比拟多的时候,复杂性也会很高。

5、ThreadLocal变量

ThreadLocal中变量的作用域是以后线程,应用线程池后会因跨线程导致数据不能传递,如果业务中应用了ThreadLocal,须要额定解决这种场景。

(1)InheritableThreadLocal

InheritableThreadLocal是在父子线程中主动传递参数,在线程池场景中不实用。

(2)手动解决

在提交工作前把ThreadLocal中的值取出来,在线程池执行时再set到线程池中线程的ThreadLocal中,并且在finally中清理数据。

毛病是每个线程池都要解决一遍,如果对上下文不相熟,有漏传的危险。

(3)TransmittableThreadLocal

阿里开源地址:TransmittableThreadLocal

原理是通过javaagent主动解决ThreadLocal跨线程池传参,对业务开发者无感知,也是举荐的计划。

6、异样解决

(1)异样感知

execute办法:抛异样会被提交工作线程感知;

submit办法:抛异样不会被提交工作线程感知,在Future.get()执行时会被感知;

(2)对立解决计划1:异步工作里对立catch

在线程池的执行逻辑最外层,包装try、catch,解决所有异样。

毛病是: 1)所有的不同工作都要trycatch,减少了代码量。2)不存在checkedexception的中央也须要都trycatch起来,代码俊俏。

(3)对立解决计划2:覆写对立异样解决办法

此计划有两种罕用实现:1)自定义线程池,继承ThreadPoolExecutor并覆写其afterExecute办法;2)创立线程池时自定义ThreadFactory,在实现里手动创立线程池,并调用Thread.setUncaughtExceptionHandler注册对立异样处理器。

(4)对立解决计划3:Future

工作提交都应用submit,并在Future.get()时捕捉所有异样。

三、总结

本文从创立线程池、队列注意事项、如何保障工作不失落、ThreadLocal、异样等方面总结了笔者的一些思考,各位读者能够对照下本人的应用场景,看本文提到的问题是否都思考到了呢,或者你还有什么线程池方面的应用教训,欢送交换分享。

本文链接:Java线程池进阶

作者简介:木小丰,美团Java技术专家,专一分享软件研发实际、架构思考。欢送关注公共号:Java研发

更多精彩文章:

从MVC到DDD的架构演进

平台化建设思路浅谈

构建可回滚的利用及上线checklist实际

Maven依赖抵触问题排查教训