一些关于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");
}
}
SpringBoot为ApplicationContextEvent提供了四种事件:
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 方法会先于 InitializingBean 的 afterPropertiesSet() 被调用。这是因为 @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/700808… github.com/chenyongyin…
动态创建bean:cloud.tencent.com/developer/a…