实习日志-线程池简单研究与自定义starter

声明:涉及到隐私问题,故该系列文章不会有关项目详细的代码实现与项目细节,仅用作个人记录。

一. 背景
最近一个任务,需要将项目中使用到的自定义线程池整合到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是一个工厂类,提供工厂和实用程序方法,与线程池的实现并没有什么关系。从源码中画类图与摘出部分方法:

image.png

忽略其他启动/中断/唤醒等方法,关注execute()与submit()。submit()仍是内部使用execute方法,不过会返回线程执行的结果。execute()则是针对Runnable,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
线程池执行过程:
image.png

饱和处理
任务拒绝策略
  当线程池的任务缓存队列已满并且线程池中的线程数目达到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

参考链接:

自动装配
Costom-SpringBoot-Starter

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。