关于动态线程池starter包的开发

216 阅读26分钟

一些关于springboot的使用

bean注册

  • @Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。

  • @ConditionalOnMissingBean 修饰bean的一个注解,当bean被注册之后如果再注册相同类型的bean就不会成功,可以保证bean实例只有一个。

  • @ConditionalOnProperty 根据配置项控制是否将当前bean注册到容器:prefix表示配置文件里节点前缀,name用来从application.properties中读取某个属性值,havingValue表示目标值。

  • @ConditionalOnBean(name="city") 只有当city注册为bean时才加载当前bean,主要用于bean之间的依赖。

  • @ConditionalOnClass(User.class) 只有当User这个bean被注入到容器才会加载当前bean。

  • @Order 控制bean的执行顺序优先级,默认值越小优先级越高。

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = DynamicConstants.CONFIG_PREFIX, name = "enable", havingValue = "true")
public AdjustExecutor getExecutor(){
    return new AdjustExecutor();
}

  • 如果一定要区分两个配置类的先后顺序,可以将这两个类交与EnableAutoConfiguration管理和触发。也就是定义在META-INF\spring.factories中声明是配置类,然后通过@AutoConfigureBefore、AutoConfigureAfter、AutoConfigureOrder控制先后顺序。因为这三个注解只对自动配置类生效。

spring factories 机制

  • resource下新建文件夹META-INF,在文件夹下面新建spring.factories文件,文件中配置,key为自定配置类EnableAutoConfiguration的全路径,value是配置类的全路径。
spring.factories内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=me.ele.newretail.kuafu.DynamicBeanRegister

其中:DynamicBeanRegister为Java config配置类。

自定义二方包中的bean无法被项目加载,可以通过springboot的自动加载机制注入:www.jianshu.com/p/464d04c36…

初始化操作

  • @PostConstruct 在一个Bean组件中,标记了@PostConstruct的方法会在Bean构造完成后自动执行方法的逻辑。

    • springBoot会把标记了Bean相关注解(如@Component)的类或接口自动初始化为全局的单一实例;在初始化过程中,执行完bean的构造方法就会执行该bean的@PostConstruct方法,然后初始化下一个bean。

    • 所以如果@PostConstruct方法耗时较高会影响到应用的启动时间。只有所有bean都加载完后springboot才会打开端口提供服务,在此之前应用不可访问。

    • 耗时的逻辑可以在@PostConstruct中启用独立线程执行

    • 也可以在CommandLineRunner或ApplicationRunner的实现组件中执行初始化操作。

  • 实现ApplicationRunner接口的Bean

@Component
public class ApplicationRunnerImpl implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // do something
    }

}
  • 实现CommandLineRunner接口的bean

@Component + implement CommandLineRunner。 springApplication的run方法会调用afterRefresh(),会触发callRunners()方法,会调用所有实现了ApplicationRunner、CommandLineRunner接口的方法。

  • 实现ApplicationListener接口:监听的事件,通常是ApplicationStartedEvent 或者ApplicationReadyEvent。
@Component
public class ApplicationListenerImpl implements ApplicationListener<ApplicationStartedEvent> {
    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        System.out.println("listener");
    }
}

SpringBootApplicationContextEvent提供了四种事件:
ApplicationStartedEvent :spring boot启动开始时执行的事件;
ApplicationEnvironmentPreparedEvent:spring boot 对应Enviroment已经准备完毕,但此时上下文context还没有创建;
ApplicationPreparedEvent:spring boot上下文context创建完成,但此时spring中的bean是没有完全加载完成的;
ApplicationFailedEvent:spring boot启动异常时执行事件。

  • 执行顺序:

注解方式@PostConstruct始终最先执行;

如果监听的是ApplicationStartedEvent事件,则一定会在CommandLineRunner和ApplicationRunner之前执行。

如果监听的是ApplicationReadyEvent事件,则一定会在CommandLineRunner和ApplicationRunner之后执行。

CommandLineRunner和ApplicationRunner 默认是ApplicationRunner先执行,如果双方指定了@Order 则按照@Order的大小顺序执行。

CommandLineRunner和ApplicationRunner都是Spring Boot中的接口,用于在Spring Boot应用程序启动后执行一些特定的代码。

异同点如下:
1、功能:两者的功能都是在应用程序启动后执行一些代码,可以用于一些初始化操作或者启动后需要立即执行的任务。
2、参数:CommandLineRunner的run方法接受一个String数组作为参数,其中包含了应用程序启动时传递的命令行参数。而ApplicationRunner的run方法接受一个ApplicationArguments对象作为参数,该对象封装了应用程序启动时传递的命令行参数和其他类型的参数。
3、使用场景:如果您的代码依赖于命令行参数,例如需要读取命令行参数进行初始化配置,那么可以选择实现CommandLineRunner接口。但如果您的代码不依赖于命令行参数,或者需要更丰富的参数信息,可以选择实现ApplicationRunner接口。
4、接口方法的返回值:CommandLineRunner的run方法没有返回值,而ApplicationRunner的run方法可以返回一个任意类型的值。

  • InitializingBean接口和BeanPostProcessor接口

这两个都是Spring框架提供的用于在Bean初始化阶段进行一些操作的接口,区别在于InitializingBean是一个简单的回调接口,Bean只需要实现它,并重写afterPropertiesSet方法,该方法在Bean的属性设置完成后被调用。在这个方法中,可以进行一些必要的初始化工作。 BeanPostProcessor是一个更为复杂的接口,用于对Bean进行自定义的处理。它定义了两个方法:postProcessBeforeInitialization和postProcessAfterInitialization。这两个方法分别在Bean的初始化前后被调用,可以在这两个方法中对Bean进行一系列的自定义处理,如修改属性值、动态代理等。

执行顺序: BeanPostProcessor的postProcessBeforeInitialization方法:它可以在Bean初始化前对Bean进行一些自定义处理。 InitializingBean的afterPropertiesSet方法:它用于在Bean的属性设置完成后进行一些必要的初始化工作。 BeanPostProcessor的postProcessAfterInitialization方法:它用于在Bean的初始化完成后进行一些自定义处理。

  • @PostConstruct和在InitializingBean接口:

@PostConstruct注解是Java EE规范的一部分,在Spring中也支持使用。通过在Bean的方法上标注@PostConstruct注解,指定该方法在Bean初始化后要执行。这个方法可以用来在Bean的属性设置完成后进行一些初始化工作。 InitializingBean接口是Spring框架提供的一个回调接口,Bean可以实现该接口,在其中重写afterPropertiesSet方法。这个方法会在Bean的属性设置完成后被自动调用,可以在其中进行一些必要的初始化操作。 执行顺序:如果一个bean同时实现了 InitializingBean 接口并且也有 @PostConstruct 方法,那么 @PostConstruct 方法会先于 InitializingBeanafterPropertiesSet() 被调用。这是因为 @PostConstruct 是在依赖注入完成后立刻触发的,而 afterPropertiesSet() 则是作为 InitializingBean 生命周期的一部分,在所有初始化标记点(包括 @PostConstruct)之后调用。

如何选择:@PostConstruct注解更为灵活,可以在任意方法上使用,而InitializingBean接口则是一种约定,需要实现该接口并重写afterPropertiesSet方法。通常来说,推荐使用@PostConstruct注解,因为它更简洁,并且不会对Bean的继承关系产生依赖。而InitializingBean接口则更适合在需要对Bean进行一些强制性初始化操作时使用。

注意:@PostConstruct只对当前类有效(只会执行一次),且标注@PostConstruct的类必须注册为spring中的bean才生效。InitializingBean接口也是仅适用于实现了该接口的具体bean。而BeanPostProcessor接口会对每个bean生效。

  • BeanFactoryPostProcessor的作用 BeanFactoryPostProcessor的作用是在Spring容器加载Bean定义后,对Bean定义进行修改或者添加新的Bean定义。它可以用于以下场景:

定义全局配置:可以使用BeanFactoryPostProcessor在容器实例化Bean之前,向容器中添加一些全局配置,比如数据源配置、日志配置等。

动态修改Bean定义:可以通过BeanFactoryPostProcessor修改Bean的属性值、修改Bean的定义等。这样可以在不修改源码的情况下,动态地修改Bean的行为。

注册自定义实例化逻辑:通过BeanFactoryPostProcessor可以注册自定义的实例化逻辑,比如根据某些条件判断是否需要创建某个Bean实例,或者根据某些条件动态生成Bean的实例。

解决循环依赖问题:通过BeanFactoryPostProcessor可以提前实例化某些Bean,从而解决循环依赖问题。例如,可以通过实现BeanFactoryPostProcessor接口,在postProcessBeanFactory方法中手动实例化A类的Bean,然后注入到B类的Bean中,从而解决A和B之间的循环依赖问题。

总的来说,BeanFactoryPostProcessor可以用于对Bean定义进行预处理,从而在容器实例化Bean之前对其进行修改、添加或者解决一些特殊问题。它提供了灵活的扩展机制,可以满足各种复杂的定制化需求。 需要注意的是,BeanFactoryPostProcessor的执行顺序在Bean的实例化之前,因此它们不能直接操作或者访问Bean的实例。它们通常用于修改或者扩展Bean的定义,以便在创建Bean实例之前对其进行一些特殊处理。

一些其他的注解

  • @ComponentScan注解告诉Spring框架在哪些包中查找组件,并将它们自动注册到Spring容器中。
  • @EnableConfigurationProperties注解
1、将@ConfigurationProperties注解标记的类添加到Spring容器中:通过@EnableConfigurationProperties注解,可以使被@ConfigurationProperties注解标记的类成为Spring容器中的一个Bean,从而可以在应用程序中进行依赖注入或其他操作。
2、启用配置文件绑定:一旦@EnableConfigurationProperties注解被应用,Spring Boot会自动执行配置文件与@ConfigurationProperties标记的类属性之间的绑定操作。这意味着,配置文件中的属性值将会被读取并绑定到相应的类属性上,方便在应用程序中使用。

通常,@EnableConfigurationProperties注解与@ConfigurationProperties注解配合使用,为应用程序提供更灵活、可配置的属性绑定功能。

@Configuration
@EnableConfigurationProperties({PangolinModuleProperties.class})
@ComponentScan({"com.xxxx.pangolin.module", "com.xxxx.pangolin.boot.module"})
public class PangolinModuleAutoConfiguration {
    @Autowired
    private PangolinModuleProperties pangolinModuleProperties;

    public PangolinModuleAutoConfiguration() {
    }

    @Bean
    public SelfCheck SelfCheckInitializing() {
        return new SelfCheck(this.pangolinModuleProperties.getBeta());
    }
}


@ConfigurationProperties(
    prefix = "pangolin.module"
)
public class PangolinModuleProperties {
    private Boolean beta;

    public PangolinModuleProperties() {
    }

    public Boolean getBeta() {
        return this.beta;
    }

    public void setBeta(Boolean beta) {
        this.beta = beta;
    }
}

  • 如何在starter开发中绑定配置类 在没有启动类的情况下,想要将@EnableConfigurationProperties注解与配置类绑定,可以使用META-INF/spring.factories文件来实现: 在spring.factories文件中添加以下内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.YourConfigurationClass    # com.example.YourConfigurationClass是你的配置类的全限定名

确保你的配置类(YourConfigurationClass)中使用了@Configuration注解,并且使用@EnableConfigurationProperties绑定了@ConfigurationProperties类。

@Configuration
@EnableConfigurationProperties(YourPropertiesClass.class)
public class YourConfigurationClass {
    // ...
}

在starter项目的pom.xml文件中,确保已经添加了spring-boot-maven-plugin插件,用于将META-INF/spring.factories文件打包到jar中。

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

这样,当其他项目引入了你的starter依赖后,Spring Boot会自动扫描META-INF/spring.factories文件,找到配置类YourConfigurationClass,并将其作为自动配置类启用,同时绑定@ConfigurationProperties类。

请注意,这种方式适用于没有启动类的情况,例如在开发Spring Boot Starter时。如果有启动类,则直接在启动类上使用@EnableConfigurationProperties注解即可。

  • ApplicationContextInitializer ApplicationContextInitializer 的回调方法 initialize 会在 Spring 容器初始化之前调用,这意味着它在任何 Bean 实例化之前就已经被执行了。 通常用于以下场景:
    • 修改配置:在上下文初始化之前添加或修改配置属性。
    • 注册额外的 Bean:在上下文初始化之前注册额外的 Bean 定义。
    • 设置环境变量:设置或修改上下文的环境变量。
    • 添加拦截器或过滤器:为 Web 应用添加额外的拦截器或过滤器。
    • 执行前置操作:执行一些前置操作,如初始化数据源等。

注意:implements ApplicationContextInitializer的类需要注册后才能使用,由于BeanFactory还没有创建,无法通过@Component注册,需要通过在src/main/resources/META-INF/spring.factories文件中指定注册项的方式来注册:

org.springframework.context.ApplicationContextInitializer= org.zyf.javabasic.springextend.runext.InterveneApplicationContextInitializer
  • SpringApplicationRunListener

SpringApplicationRunListener是Spring Boot的一个事件监听器,用于在应用程序启动和停止时执行一些操作。可能需要自定义SpringApplicationRunListener来执行某些特定操作。

方法及执行时机

1、starting() 执行时机:在 SpringApplication 的 run() 方法开始执行时调用。 描述:表示 Spring Boot 应用启动的开始。此时,尚未开始任何实际的初始化工作。 environmentPrepared(ConfigurableEnvironment environment) 执行时机:在环境(Environment)配置准备好之后调用。 描述:此时,Spring Boot 已经完成了对应用环境的配置,包括从命令行参数、配置文件、系统属性等来源加载配置信息。可以在此处进一步修改环境配置。

2、contextPrepared(ConfigurableApplicationContext context) 执行时机:在应用上下文(ApplicationContext)准备好了之后调用。 描述:此时,应用上下文已经创建,但尚未加载任何 Bean。可以在此处向上下文中添加额外的 Bean 或配置。 contextLoaded(ConfigurableApplicationContext context) 执行时机:在应用上下文已经加载了所有的 Bean 定义之后调用。 描述:此时,所有的 Bean 定义已经被加载到了上下文中,但 Bean 还未被实例化。可以在此处进一步修改上下文配置。

3、started(ConfigurableApplicationContext context) 执行时机:在应用上下文已经被刷新(refresh)并且所有 Bean 已经初始化之后调用。 描述:此时,应用上下文已经完全初始化,并且所有的 Bean 都已经被实例化和初始化。可以在此处执行一些应用启动后的初始化逻辑。

4、running(ConfigurableApplicationContext context) 执行时机:在应用上下文已经被刷新并且所有的 CommandLineRunner 和 ApplicationRunner 组件都已执行之后调用。 描述:此时,应用已经完全准备好接受请求。可以在此处执行一些启动后的最终逻辑。

5、failed(ConfigurableApplicationContext context, Throwable exception) 执行时机:如果启动过程中出现异常,则会调用此方法。 描述:表示启动失败,并且抛出了异常。可以在此处进行错误处理或记录日志等操作。

使用场景 SpringApplicationRunListener 主要用于跟踪 Spring Boot 应用程序启动的不同阶段,并在这些阶段执行特定的逻辑。例如: 记录启动日志:可以在 starting() 方法中记录启动开始的信息,在 running() 方法中记录启动完成的信息。 修改配置:可以在 environmentPrepared() 或 contextPrepared() 中修改环境或上下文配置。 执行预启动逻辑:可以在 contextLoaded() 或 started() 中执行一些预启动的初始化逻辑。 处理启动失败的情况:可以在 failed() 中处理启动失败的异常情况。

实现了SpringApplicationRunListener的接口也需要到src/main/resources/META-INF/spring.factories中注册:

org.springframework.boot.SpringApplicationRunListener= org.zyf.javabasic.springextend.runext.IntervenRunListener

示例代码:

public class IntervenRunListener implements SpringApplicationRunListener { private final SpringApplication application; private final String[] args; public IntervenRunListener(SpringApplication application, String[] args) { this.application = application; this.args = args; } @Override public void starting() { System.out.println("IntervenRunListener starting"); } @Override public void environmentPrepared(ConfigurableEnvironment environment) { System.out.println("IntervenRunListener environmentPrepared"); } @Override public void contextPrepared(ConfigurableApplicationContext context) { System.out.println("IntervenRunListener contextPrepared"); } @Override public void contextLoaded(ConfigurableApplicationContext context) { System.out.println("IntervenRunListener contextLoaded"); } @Override public void started(ConfigurableApplicationContext context) { System.out.println("IntervenRunListener started"); } @Override public void running(ConfigurableApplicationContext context) { System.out.println("IntervenRunListener running"); } @Override public void failed(ConfigurableApplicationContext context, Throwable exception) { System.out.println("IntervenRunListener failed"); } }

配置项

@ConfigurationProperties 可以作用在类或方法上,用于读取yml中配置项。 配置项采用了宽松绑定规则,对于驼峰式或下划线等格式都可以读取。

  • 作用在类上
方式一:通过@ConfigurationProperties设置要读取的配置项前缀,并将配置类声明为一个bean组件。

@Data
@Component
@ConfigurationProperties(prefix = DynamicConstants.CONFIG_PREFIX)
public class DynamicProperties {

    /**
     * 总开关
     */
    private boolean enable;

    private String desc;

}

方式二:
通过@ConfigurationProperties设置要读取的配置项前缀,并且在Java config中使用@EnableConfigurationProperties让配置类被springboot知道。

@Configuration
@EnableConfigurationProperties(DynamicProperties.class)
public class DynamicBeanRegister {

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = DynamicConstants.CONFIG_PREFIX, name = "enable", havingValue = "true")
    public AdjustExecutor getExecutor(){
        return new AdjustExecutor();
    }
}

@EnableConfigurationProperties的作用是使 DynamicProperties 这个类上标注的 @ConfigurationProperties 注解生效,并且会自动将这个类注入到 IOC 容器中.

  • 作用在方法上 该注解作用于方法上时,如果想要有效的绑定配置,那么该方法需要有@Bean注解且所属Class需要有@Configuration注解。
@Configuration
public class DruidDataSourceConfig {
    /**
     * DataSource 配置
     * @return
     */
    @ConfigurationProperties(prefix = "spring.datasource.druid.read")
    @Bean(name = "readDruidDataSource")
    public DataSource readDruidDataSource() {
        return new DruidDataSource();
    }
}


获取Context上下文

  • 实现了ApplicationContextAware接口的bean,当spring容器初始化的时候,会自动的将ApplicationContext注入进来。
@Component
public class AdjustExecutor implements ApplicationContextAware, ApplicationRunner {

    private ApplicationContext applicationContext;

    public static final Map<String, ExecutorService> POOL = new HashMap<>();

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    
    @Override
    public void run(ApplicationArguments args) throws Exception {
        Map<String, ThreadPoolExecutor> beansOfType = applicationContext.getBeansOfType(ThreadPoolExecutor.class);
        for (Map.Entry<String, ThreadPoolExecutor> entry : beansOfType.entrySet()) {
            Adjustable adjustable = entry.getValue().getClass().getAnnotation(Adjustable.class);
            if (Objects.nonNull(adjustable) && Boolean.TRUE.equals(adjustable.enable())){
                POOL.put(entry.getKey(), entry.getValue());
            }
        }

        // 添加diamond监听任务等
        
    }
}

Java线程池

实现并发:Actor模型(仅在scala中有应用)、多线程、协程(java支持较弱)。

ThreadPoolExecutor:共有7个参数,corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler;这些参数都使用volatile修饰。

执行原理:先调用core线程执行,超过coreSize后放到缓冲队列中,队列满后调用max线程执行,如果仍然无法处理则触发拒绝策略。

核心参数:corePoolSize、maximumPoolSize,workQueue; 对于需要快速响应的接口,建议调大处理线程,使用SynchronousQueue;对于并行执行大批次(需要大IO)的接口可以使用有界队列起到缓冲作用,此时如果处理线程过大会增加上下文切换成本。 线程池队列占用的是堆内存,要注意jvm内存大小及gc能力,尽量减少大对象的存在。


// 修改核心线程数
public void setCorePoolSize(int corePoolSize) {
    if (corePoolSize < 0)
        throw new IllegalArgumentException();
    int delta = corePoolSize - this.corePoolSize;
    this.corePoolSize = corePoolSize;
    // 核心线程调小,中断空闲任务;否则任务执行结束自动调小
    if (workerCountOf(ctl.get()) > corePoolSize)
        interruptIdleWorkers();
    // 核心线程数调大
    else if (delta > 0) {
        // We don't really know how many new threads are "needed".
        // As a heuristic, prestart enough new workers (up to new
        // core size) to handle the current number of tasks in
        // queue, but stop if queue becomes empty while doing so.
        int k = Math.min(delta, workQueue.size());
        while (k-- > 0 && addWorker(null, true)) {
            if (workQueue.isEmpty())
                break;
        }
    }
}

// 修改最大线程数
public void setMaximumPoolSize(int maximumPoolSize) {
    if (maximumPoolSize <= 0 || maximumPoolSize < corePoolSize)
        throw new IllegalArgumentException();
    this.maximumPoolSize = maximumPoolSize;
    if (workerCountOf(ctl.get()) > maximumPoolSize)
        interruptIdleWorkers();
}


32位整数

Doug Lea 采用一个 32 位的整数来存放线程池的状态和当前池中的线程数,其中高 3 位用于存放线程池状态,低 29 位表示线程数。


private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

// 这里 COUNT_BITS 设置为 29(32-3),意味着前三位用于存放线程状态,后29位用于存放线程数
// 很多初学者很喜欢在自己的代码中写很多 29 这种数字,或者某个特殊的字符串,然后分布在各个地方,这是非常糟糕的
private static final int COUNT_BITS = Integer.SIZE - 3;

// 000 11111111111111111111111111111
// 这里得到的是 29 个 1,也就是说线程池的最大线程数是 2^29-1=536870911
// 以我们现在计算机的实际情况,这个数量还是够用的
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// 我们说了,线程池的状态存放在高 3 位中
// 运算结果为 111跟29个0:111 00000000000000000000000000000
private static final int RUNNING    = -1 << COUNT_BITS;
// 000 00000000000000000000000000000
private static final int SHUTDOWN   =  0 << COUNT_BITS;
// 001 00000000000000000000000000000
private static final int STOP       =  1 << COUNT_BITS;
// 010 00000000000000000000000000000
private static final int TIDYING    =  2 << COUNT_BITS;
// 011 00000000000000000000000000000
private static final int TERMINATED =  3 << COUNT_BITS;

// 将整数 c 的低 29 位修改为 0,就得到了线程池的状态
private static int runStateOf(int c)     { return c & ~CAPACITY; }
// 将整数 c 的高 3 为修改为 0,就得到了线程池中的线程数
private static int workerCountOf(int c)  { return c & CAPACITY; }

private static int ctlOf(int rs, int wc) { return rs | wc; }

线程池状态

  • RUNNING:这个没什么好说的,这是最正常的状态:接受新的任务,处理等待队列中的任务;
  • SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务;
  • STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程;
  • TIDYING:所有的任务都销毁了,workCount 为 0。线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated();
  • TERMINATED:terminated() 方法结束后,线程池的状态就会变成这个。

RUNNING 定义为 -1,SHUTDOWN 定义为 0,其他的都比 0 大,所以等于 0 的时候不能提交任务,大于 0 的话,连正在执行的任务也需要中断。

execute和addworker

如果某个任务执行出现异常,那么执行任务的线程会被关闭,而不是继续接收其他任务。然后会启动一个新的线程来代替它。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    // 前面说的那个表示 “线程池状态” 和 “线程数” 的整数
    int c = ctl.get();

    // 如果当前线程数少于核心线程数,那么直接添加一个 worker 来执行任务,
    // 创建一个新的线程,并把当前任务 command 作为这个线程的第一个任务(firstTask)
    if (workerCountOf(c) < corePoolSize) {
        // 添加任务成功,那么就结束了。提交任务嘛,线程池已经接受了这个任务,这个方法也就可以返回了
        // 至于执行的结果,到时候会包装到 FutureTask 中。
        // 返回 false 代表线程池不允许提交任务
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 到这里说明,要么当前线程数大于等于核心线程数,要么刚刚 addWorker 失败了

    // 如果线程池处于 RUNNING 状态,把这个任务添加到任务队列 workQueue 中
    if (isRunning(c) && workQueue.offer(command)) {
        /* 这里面说的是,如果任务进入了 workQueue,我们是否需要开启新的线程
         * 因为线程数在 [0, corePoolSize) 是无条件开启新的线程
         * 如果线程数已经大于等于 corePoolSize,那么将任务添加到队列中,然后进到这里
         */
        int recheck = ctl.get();
        // 如果线程池已不处于 RUNNING 状态,那么移除已经入队的这个任务,并且执行拒绝策略
        if (! isRunning(recheck) && remove(command))
            reject(command);
        // 如果线程池还是 RUNNING 的,并且线程数为 0,那么开启新的线程
        // 到这里,我们知道了,这块代码的真正意图是:担心任务提交到队列中了,但是线程都关闭了
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 如果 workQueue 队列满了,那么进入到这个分支
    // 以 maximumPoolSize 为界创建新的 worker,
    // 如果失败,说明当前线程数已经达到 maximumPoolSize,执行拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}

// 第一个参数是准备提交给这个线程执行的任务,之前说了,可以为 null
// 第二个参数为 true 代表使用核心线程数 corePoolSize 作为创建线程的界限,也就说创建这个线程的时候,
//         如果线程池中的线程总数已经达到 corePoolSize,那么不能响应这次创建线程的请求
//         如果是 false,代表使用最大线程数 maximumPoolSize 作为界限
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 这个非常不好理解
        // 如果线程池已关闭,并满足以下条件之一,那么不创建新的 worker:
        // 1. 线程池状态大于 SHUTDOWN,其实也就是 STOP, TIDYING, 或 TERMINATED
        // 2. firstTask != null
        // 3. workQueue.isEmpty()
        // 简单分析下:
        // 还是状态控制的问题,当线程池处于 SHUTDOWN 的时候,不允许提交任务,但是已有的任务继续执行
        // 当状态大于 SHUTDOWN 时,不允许提交任务,且中断正在执行的任务
        // 多说一句:如果线程池处于 SHUTDOWN,但是 firstTask 为 null,且 workQueue 非空,那么是允许创建 worker 的
        // 这是因为 SHUTDOWN 的语义:不允许提交新的任务,但是要把已经进入到 workQueue 的任务执行完,所以在满足条件的基础上,是允许创建新的 Worker 的
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // 如果成功,那么就是所有创建线程前的条件校验都满足了,准备创建线程执行任务了
            // 这里失败的话,说明有其他线程也在尝试往线程池中创建线程
            if (compareAndIncrementWorkerCount(c))
                break retry;
            // 由于有并发,重新再读取一下 ctl
            c = ctl.get();
            // 正常如果是 CAS 失败的话,进到下一个里层的for循环就可以了
            // 可是如果是因为其他线程的操作,导致线程池的状态发生了变更,如有其他线程关闭了这个线程池
            // 那么需要回到外层的for循环
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    /* 
     * 到这里,我们认为在当前这个时刻,可以开始创建线程来执行任务了,
     * 因为该校验的都校验了,至于以后会发生什么,那是以后的事,至少当前是满足条件的
     */

    // worker 是否已经启动
    boolean workerStarted = false;
    // 是否已将这个 worker 添加到 workers 这个 HashSet 中
    boolean workerAdded = false;
    Worker w = null;
    try {
        final ReentrantLock mainLock = this.mainLock;
        // 把 firstTask 传给 worker 的构造方法
        w = new Worker(firstTask);
        // 取 worker 中的线程对象,之前说了,Worker的构造方法会调用 ThreadFactory 来创建一个新的线程
        final Thread t = w.thread;
        if (t != null) {
            // 这个是整个线程池的全局锁,持有这个锁才能让下面的操作“顺理成章”,
            // 因为关闭一个线程池需要这个锁,至少我持有锁的期间,线程池不会被关闭
            mainLock.lock();
            try {

                int c = ctl.get();
                int rs = runStateOf(c);

                // 小于 SHUTTDOWN 那就是 RUNNING,这个自不必说,是最正常的情况
                // 如果等于 SHUTDOWN,前面说了,不接受新的任务,但是会继续执行等待队列中的任务
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    // worker 里面的 thread 可不能是已经启动的
                    if (t.isAlive())
                        throw new IllegalThreadStateException();
                    // 加到 workers 这个 HashSet 中
                    workers.add(w);
                    int s = workers.size();
                    // largestPoolSize 用于记录 workers 中的个数的最大值
                    // 因为 workers 是不断增加减少的,通过这个值可以知道线程池的大小曾经达到的最大值
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            // 添加成功的话,启动这个线程
            if (workerAdded) {
                // 启动线程
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        // 如果线程没有启动,需要做一些清理工作,如前面 workCount 加了 1,将其减掉
        if (! workerStarted)
            addWorkerFailed(w);
    }
    // 返回线程是否启动成功
    return workerStarted;
}

worker 中的线程 start 后,其 run 方法会调用 runWorker 方法:

// 此方法由 worker 线程启动后调用,这里用一个 while 循环来不断地从等待队列中获取任务并执行
// 前面说了,worker 在初始化的时候,可以指定 firstTask,那么第一个任务也就可以不需要从队列中获取
final void runWorker(Worker w) {
    // 
    Thread wt = Thread.currentThread();
    // 该线程的第一个任务(如果有的话)
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        // 循环调用 getTask 获取任务
        while (task != null || (task = getTask()) != null) {
            w.lock();          
            // 如果线程池状态大于等于 STOP,那么意味着该线程也要中断
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                // 这是一个钩子方法,留给需要的子类实现
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 到这里终于可以执行任务了
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    // 这里不允许抛出 Throwable,所以转换为 Error
                    thrown = x; throw new Error(x);
                } finally {
                    // 也是一个钩子方法,将 task 和异常作为参数,留给需要的子类实现
                    afterExecute(task, thrown);
                }
            } finally {
                // 置空 task,准备 getTask 获取下一个任务
                task = null;
                // 累加完成的任务数
                w.completedTasks++;
                // 释放掉 worker 的独占锁
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        // 如果到这里,需要执行线程关闭:
        // 1. 说明 getTask 返回 null,也就是说,队列中已经没有任务需要执行了,执行关闭
        // 2. 任务执行过程中发生了异常
        // 第一种情况,已经在代码处理了将 workCount 减 1,这个在 getTask 方法分析中会说
        // 第二种情况,workCount 没有进行处理,所以需要在 processWorkerExit 中处理
        // 限于篇幅,我不准备分析这个方法了,感兴趣的读者请自行分析源码
        processWorkerExit(w, completedAbruptly);
    }
}

我们看看 getTask() 是怎么获取任务的,这个方法写得真的很好,每一行都很简单,组合起来却所有的情况都想好了:

// 此方法有三种可能:
// 1. 阻塞直到获取到任务返回。我们知道,默认 corePoolSize 之内的线程是不会被回收的,
//      它们会一直等待任务
// 2. 超时退出。keepAliveTime 起作用的时候,也就是如果这么多时间内都没有任务,那么应该执行关闭
// 3. 如果发生了以下条件,此方法必须返回 null:
//    - 池中有大于 maximumPoolSize 个 workers 存在(通过调用 setMaximumPoolSize 进行设置)
//    - 线程池处于 SHUTDOWN,而且 workQueue 是空的,前面说了,这种不再接受新的任务
//    - 线程池处于 STOP,不仅不接受新的线程,连 workQueue 中的线程也不再执行
private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        // 两种可能
        // 1. rs == SHUTDOWN && workQueue.isEmpty()
        // 2. rs >= STOP
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            // CAS 操作,减少工作线程数
            decrementWorkerCount();
            return null;
        }

        boolean timed;      // Are workers subject to culling?
        for (;;) {
            int wc = workerCountOf(c);
            // 允许核心线程数内的线程回收,或当前线程数超过了核心线程数,那么有可能发生超时关闭
            timed = allowCoreThreadTimeOut || wc > corePoolSize;

            // 这里 break,是为了不往下执行后一个 if (compareAndDecrementWorkerCount(c))
            // 两个 if 一起看:如果当前线程数 wc > maximumPoolSize,或者超时,都返回 null
            // 那这里的问题来了,wc > maximumPoolSize 的情况,为什么要返回 null?
            //    换句话说,返回 null 意味着关闭线程。
            // 那是因为有可能开发者调用了 setMaximumPoolSize() 将线程池的 maximumPoolSize 调小了,那么多余的 Worker 就需要被关闭
            if (wc <= maximumPoolSize && ! (timedOut && timed))
                break;
            if (compareAndDecrementWorkerCount(c))
                return null;
            c = ctl.get();  // Re-read ctl
            // compareAndDecrementWorkerCount(c) 失败,线程池中的线程数发生了改变
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
        // wc <= maximumPoolSize 同时没有超时
        try {
            // 到 workQueue 中获取任务
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            // 如果此 worker 发生了中断,采取的方案是重试
            // 解释下为什么会发生中断,这个读者要去看 setMaximumPoolSize 方法。

            // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量,
            // 那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null
            timedOut = false;
        }
    }
}

关于动态线程池

参考:美团动态线程池框架(DynamicTp) dynamictp.cn/guide/use/t…

cloud.tencent.com/developer/a…

juejin.cn/post/706340…

github.com/dromara/dyn…

一个简版的实现,参考意义不大: juejin.cn/post/700808… github.com/chenyongyin…

动态创建bean:cloud.tencent.com/developer/a…