最近看到美团技术团队的动态线程池分析文章:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
以及一个对应的开源项目:https://github.com/dromara/dynamic-tp
有点意思,同时也觉得动态线程池在工作的实用性,便通过此文来分析一下动态线程池的核心实现原理,本文参考了美团的文章和开源项目的实现思路,特此感谢。
动态线程池,指的是线程池中的参数可以动态修改并生效,比如corePoolSize、maximumPoolSize等。
在工作中,线程池的核心线程数和最大线程数等参数是很难估计和固定的,如果能在应用运行过程中动态进行调整,也就很有必要了。
核心配置项
dtp:
enable: true
core-pool-size: 10
maximum-pool-size: 50
我希望,能通过以上配置就能配置出一个动态线程池:
另外,我希望能做到,只有项目的配置中存在dtp配置,并且enable不等于false,那就表示开启动态线程池,就需要向Spring容器中添加一个线程池对象作为一个Bean对象,这样其他Bean就能通过依赖注入来使用动态线程池了。
另外,对于上面的配置,我们最好是配置在nacos中,这样才能动态修改。
然后把user改写为一个SpringBoot应用:
引入依赖:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.3.12.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
新建启动类和Controller:
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@RestController
public class ZhouyuController {
@GetMapping("/test")
public String test(){
return "hello";
}
}
现在,我喜欢能在ZhouyuController中使用动态线程池,就像如下:
@RestController
public class ZhouyuController {
@Autowired
private ThreadPoolExecutor threadPoolExecutor; // 需要一个动态线程池
@GetMapping("/test")
public String test(){
threadPoolExecutor.execute(() -> {
System.out.println("执行任务");
});
return "hello";
}
}
这段要能工作,得有几个条件:
这里就引出一个问题,我们到底该如何表示一个动态线程池,动态线程池和普通线程池的区别在于,动态线程池能支持通过nacos来修改其参数。
那我们是不是需要新定义一个类来表示动态线程池呢?我给的答案是需要,因为如果不新定义一个,那么对于上述代码,如果我Spring容器中存在多个ThreadPoolExecutor类型的Bean对象,那么该如何找到动态线程池呢?只能通过属性名字了,比如属性名字为dynamicThreadPoolExecutor,这样也就需要我们在往Spring容器注册动态线程池对象时,对于的beanName一定得是dynamicThreadPoolExecutor。
而如果我们新定义一个类(dtp-aucotoncifuration项目中):
public class DtpExecutor extends ThreadPoolExecutor {
public DtpExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
}
那么如果我们想要用动态线程池就方便了:
@RestController
public class ZhouyuController {
@Autowired
private DtpExecutor dtpExecutor; // 需要一个动态线程池
@GetMapping("/test")
public String test(){
dtpExecutor.execute(() -> {
System.out.println("执行任务");
});
return "hello";
}
}
这样,代码看起来就更加明确了。
注意,user中添加依赖:
<dependency>
<groupId>org.examplegroupId>
<artifactId>dtp-autoconfigurationartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
接下来,我们再来创建DtpExecutor对象并添加到Spring容器中,这一步是非常重要的。
如果应用要开启动态线程池,那么就需要做一步,否则就不需要做这一步,并且在创建DtpExecutor对象时,得用配置的参数,并且得支持Nacos,并且还得放到Spring容器中。
这里就可以用到SpringBoot的自动配置类了。
首先在dtp-autoconfiguration中添加spring-boot的依赖:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.3.12.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
dependencies>
并且新建一个自动配置类:
@Configuration
@ConditionalOnProperty(prefix = "dtp", value = "enable", havingValue = "true")
public class DtpAutoConfiguration {
}
表示这个配置类,只有在dtp.anable=true的时候才会生效,没有这个配置项或为false则不会生效。
然后我们可以就可以在DtpAutoConfiguration中来定义DtpExecutor的Bean了,我们首先想到的就是利用@Bean,比如:
@Bean
public DtpExecutor dtpExecutor(){
DtpExecutor dtpExecutor = new DtpExecutor();
return dtpExecutor;
}
但是,DtpExecutor中是没有无参构造方法的,也就是在构造DtpExecutor对象时,我们需要能够拿到配置项参数,那怎么拿呢?
熟悉Spring的同学,可能会想到利用Enviroment对象,因为不管我们在应用程序本地的application.yaml中的配置项,还是在nacos中的配置项,最终都是放在了Environment对象中,比如如下代码:
@Configuration
@ConditionalOnProperty(prefix = "dtp", value = "enable", havingValue = "true")
public class DtpAutoConfiguration {
@Autowired
private Environment environment;
@Bean
public DtpExecutor dtpExecutor(){
Integer corePoolSize = Integer.valueOf(environment.getProperty("dpt.core-pool-size"));
Integer maximumPoolSize = Integer.valueOf(environment.getProperty("dpt.maximum-pool-size"));
DtpExecutor dtpExecutor = new DtpExecutor(corePoolSize, maximumPoolSize);
return dtpExecutor;
}
}
我们先来测试一下,注意DtpAutoConfiguration自动配置类要能够生效,还需要利用spring.factories:
因为我们有限制,所以我们还得在user中配置dtp:
dtp:
enable: true
core-pool-size: 10
maximum-pool-size: 50
并且把ZhouyuController的代码也大概改一下:
@RestController
public class ZhouyuController {
@Autowired
private DtpExecutor dtpExecutor; // 需要一个动态线程池
@GetMapping("/test")
public Integer test(){
return dtpExecutor.getCorePoolSize();
}
}
这样就能测出来,能不能使用动态线程池,并且是否是我们所配置的参数。
启动User应用,然后访问localhost:8080/test,结果为:
发现,结果正常。
并且,我们可以试试在nacos中进行配置,那我们需要在user中引入nacos-client:
<dependency>
<groupId>com.alibaba.bootgroupId>
<artifactId>nacos-config-spring-boot-starterartifactId>
<version>0.2.7version>
dependency>
然后配置nacos:
nacos:
config:
server-addr: 127.0.0.1:8848
data-id: dtp.yaml
type: yaml
auto-refresh: true
bootstrap:
enable: true
启动user应用,访问localhost:8080/test:
结果也是正常的。
也就是说,代码写到这,我们完成了:
那么最核心的问题还没有解决:应用在运行过程中,如果在nacos中修改了配置,如何生效?
这就需要在user应用中能够发现nacos中配置内容是否修改了,这就需要利用到nacos的监听器机制,我们在auto-configuration模块来定义一个nacos的监听器,这就需要auto-configuratino也依赖nacos-client,我们直接nacos-client的依赖从user模块转移到auto-configuration模块中去,这样对于user是没有影响的,因为user依赖了auto-configuration模块,从而间接的依赖了nacos-client。
我们新建一个Nacos监听器NacosRefresher:
public class NacosRefresher implements Listener, InitializingBean {
@NacosInjected
private ConfigService configService;
// 利用Spring的Bean初始化机制,来设置要监听的nacos的dataid
// 暂时写死,最好是拿到程序员所配置的dataid和group
@Override
public void afterPropertiesSet() throws NacosException {
configService.addListener("dtp.yaml", "DEFAULT_GROUP", this);
}
// 这个是Nacos收到变更事件异步执行逻辑要用到的线程池,跟动态线程池没关系
@Override
public Executor getExecutor() {
return Executors.newFixedThreadPool(1);
}
// 这是用来接收数据变更的,content就是变更后的内容
@Override
public void receiveConfigInfo(String content) {
System.out.println(content);
}
}
另外在DtpAutoConfiguration中定义NacosRefresher为一个Bean:
@Bean
public NacosRefresher nacosRefresher(){
return new NacosRefresher();
}
也可以利用@Import来导入NacosRefresher:
@Configuration
@ConditionalOnProperty(prefix = "dtp", value = "enable", havingValue = "true")
@Import(NacosRefresher.class)
public class DtpAutoConfiguration {
@Autowired
private Environment environment;
@Bean
public DtpExecutor dtpExecutor(){
Integer corePoolSize = Integer.valueOf(environment.getProperty("dtp.core-pool-size"));
Integer maximumPoolSize = Integer.valueOf(environment.getProperty("dtp.maximum-pool-size"));
DtpExecutor dtpExecutor = new DtpExecutor(corePoolSize, maximumPoolSize);
return dtpExecutor;
}
}
NacosRefresher一旦就到了数据变更事件,那么就可以更新DtpExecutor了,那么这里我们要解决两个问题:
我们先启动应用,修改一下nacos的配置,看看context长什么样:
我只是修改了core-pool-size,但是content是怎么dtp.yaml的内容,那么我们就来进行解析:
@Override
public void receiveConfigInfo(String content) {
YamlPropertiesFactoryBean bean = new YamlPropertiesFactoryBean();
bean.setResources(new ByteArrayResource(content.getBytes()));
Properties properties = bean.getObject();
System.out.println(properties);
}
这样,我们就将content解析为了Properties的格式,这样就能更加方便获取配置项了。
接下来,我们只要能拿到Spring容器中的DtpExecutor对象,那么该如何拿到呢,这里参考开源项目dynamic-tp的做法,利用BeanPostProcessor来存入到一个static的map中。
首先新建一个DtpUtil:
public class DtpUtil {
public static DtpExecutor dtpExecutor;
public static DtpExecutor get() {
return dtpExecutor;
}
public static void set(DtpExecutor dtpExecutor) {
DtpUtil.dtpExecutor = dtpExecutor;
}
}
然后新建一个BeanPostProcessor,会把DtpExecutor对象存入DtpUtil:
public class DtpBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof DtpExecutor) {
DtpUtil.set((DtpExecutor) bean);
}
return bean;
}
}
另外在DtpAutoConfiguration导入DtpBeanPostProcessor。
然后会到NacosRefresher中,我们就可以利用DtpUtil获取到DtpExecutor对象了,并且可以修改对应的参数:
@Override
public void receiveConfigInfo(String content) {
YamlPropertiesFactoryBean bean = new YamlPropertiesFactoryBean();
bean.setResources(new ByteArrayResource(content.getBytes()));
Properties properties = bean.getObject();
DtpExecutor dtpExecutor = DtpUtil.get();
dtpExecutor.setCorePoolSize(Integer.parseInt(properties.getProperty("dtp.core-pool-size")));
dtpExecutor.setMaximumPoolSize(Integer.parseInt(properties.getProperty("dtp.maximum-pool-size")));
}
这样就完成了DtpExecutor的参数修改,到此,一个简单的动态线程池就完成了,大家可以自行测试一下,修改Nacos配置,看controller那边能不能实时拿到最新的corePoolSize,我测是没问题的。
对于实现一个动态线程池,核心要点为:
比如ThreadPoolExecutor的setCorePoolSize:
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;
}
}
}
它会判断:
而所谓的动态线程池,其实就是动态的去修改线程池中的线程数量,少了就增加,多了就中断。