声明:涉及到隐私问题,故该系列文章不会有关项目详细的代码实现与项目细节,仅用作个人记录。
一. 背景
最近一个任务,需要将项目中使用到的自定义线程池整合到jar包中,后续如果要使用该线程池,只需引入jar包即可,不必再在项目中。
要求能够在项目中直接使用到线程池的类,且能够使用配置文件配置线程池的参数。
二. 实现
第一想法是使用@Value与@Configuration等注解来配合使用,直接将现有的耦合代码移入到工具包中,打成jar包后在项目中直接使用@ComponentScan注解扫描移入类所在包即可。简单暴力的完成。
经过师兄review后,使用@ComponentScan存在代码侵入,不够优雅。指导参考spring-boot-starter
的自动装配的方式来实现。
实现二:使用spring-boot-starter
的方式来实现装配相应类到容器中。实现简单,不搬运了。
三. 针对二中步骤出现的问题
想法一中不使用@ComponentScan的方式,使用新建一个注解类@Configuration,里面使用@Bean的方式注入线程池相关类到容器中,结果出现 无法实例化xxx类 ,这些类中均使用到线程池的类,而线程池注解类中又使用了一个参数配置的Bean。 这是由于使用@Bean方式会存在一个类的加载顺序问题,在xxx类初始化的时候找不到其他类出现异常。可以通过@Order该变初始化顺序但这会对现有代码侵入更大,另外也可以使用@DependsOn来指定类依赖 ,方案舍弃。
实现二中起初存在一个注入类的NPE问题。这是在 启动类中 设置 一个实例的属性为applicationcontext,在注解类中使用了该实例的的 context属性。在未启动成功时 启动类的main()方法未执行完,因此 该实例的context=null。出现NPE。最后改变实现方法。
四. Java线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池使得线程可以复用。
普通创建线程池:
ExecutorService service1= Executors.newSingleThreadExecutor();
ExecutorService service2= Executors.newFixedThreadPool(10);
进入创建线程池的实现方法:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
对ThreadPoolExecutor很明显是ExecutorService子类,点入该类:
对参数不同的构造方法举一例说明:
public ThreadPoolExecutor(int corePoolSize, //核心池的大小,保留在池中的线程数。
int maximumPoolSize, //线程池中最大的运行线程数量
long keepAliveTime, //超出corePoolSize的多余线程数量的线程生命时间
TimeUnit unit, //时间单位
BlockingQueue<Runnable> workQueue) // 线程数量超出max后等待的队列
ThreadFactory threadFactory, //自定义线程生产工厂
RejectedExecutionHandler handler //当达到max数量与队列满了仍有线程请求,拒绝服务并调用该handler
{
//略
}
针对上述参数即可自定义线程池。另外,结合我的使用过程,我对于线程池的几个类很容易搞混和搞不清晰,针对源码刨析,从最简单的使用方式ExecutorService service1
一路点上去。
ThreadPoolExecutor
-->AbstractExecutorService
-->ExecutorService
-->Executor
Executors
是一个工厂类,提供工厂和实用程序方法,与线程池的实现并没有什么关系。从源码中画类图与摘出部分方法:
忽略其他启动/中断/唤醒等方法,关注execute()与submit()。submit()仍是内部使用execute方法,不过会返回线程执行的结果。execute()则是针对Runnable,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
线程池执行过程:
饱和处理
任务拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
1 :ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
2 :ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
3 :ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务
4 :ThreadPoolExecutor.CallerRunsPolicy:如果线程池未关闭,直接runnable.run();
上述4个策略点RejectExcutionHandler
都可以看到实现方法。另外自己自定义handler实现该接口即可。
包括线程构造器等等,均可实现接口即可。(源码大法好)
针对线程池默认参数,有计算公式可以通过机器环境来计算得出最佳效率的corePoolSize。
五. 自定义Runnable任务--装饰者模式
使用装饰者模式包装Runnable,进行个性化Runnable。例如:如果我们要追踪请求的执行日志,日志是通过记录请求线程的统一标号记录的,那么当该线程产生子线程后,子线程的标号记录应该与夫线程一致。因此需要让创建子线程的时候给子线程的标号设置成父线程的(通过ThreadLocal传递)。
public class RunnableWrapper implements Runnable {
final Runnable runnable;
public RunnableWrapper(Runnable runnable) {
this.runnable = runnable;
}
public static RunnableWrapper of(Runnable runnable) {
return new RunnableWrapper(runnable);
}
@Override
public void run() {
//包装的方法,把父子标号同步
myFun();
runnable.run(); //执行传递进来的任务
}
}
如上,使用装饰者模式,对任务添加自己的个性操作。后面则对线程任务则通过多态 Runnable runnable= new RunnableWrapper()
六. 线程池原理
这一部分就是jdk的源码了,不累赘了。
多看些项目代码,才知道设计模式如何给自己所用。就像RunnableWrapper的功能,线程池使用corePoolSize和max和缓存队列的缓存策略的思想。
七.关于线程池默认大小
关注于 计算密集型和I/O密集型 方案。
详细link
参考链接: