乐趣区

关于java:Java线程池进阶

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

一、线程池工作流程

以下是 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 依赖抵触问题排查教训

退出移动版