一、串行化查询
假如任务1用时1s,任务2用时2s,任务3用时3s,那么查询结果总耗时6s,执行过程如下:
package com.example.demo.entity;
import lombok.Data;
@Data
public class Customer {
private String name;
private String orderInfo;
private String score;
}
import com.example.demo.entity.Customer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {
/**
* 任务1
*/
static String getName() {
try {
// 停止执行1秒钟
TimeUnit.SECONDS.sleep(1); // 停止执行1秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
return "name";
}
/**
* 任务2
*/
static String getScore() {
try {
// 停止执行2秒钟
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "score";
}
/**
* 任务3
*/
static String getOrderInfo() {
try {
// 停止执行3秒钟
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "orderInfo";
}
@GetMapping("getAllInfoV1")
public Boolean getAllInfoV1() {
StopWatch stopWatch = new StopWatch();
stopWatch.start("串行化查询");
Customer customer = new Customer();
//任务1
customer.setName(getName());
//任务2
customer.setOrderInfo(getOrderInfo());
//任务3
customer.setScore(getScore());
stopWatch.stop();
log.info("TaskName: " + stopWatch.getLastTaskName() + " --》 耗时(单位:秒):" + stopWatch.getTotalTimeSeconds() + ",耗时(单位:毫秒):" + stopWatch.getTotalTimeMillis());
return Boolean.TRUE;
}
}
调用接口进行测试,业务代码执行了6秒。
二、优化方案:并行执行
可以使用CompletableFuture来并发执行多个异步任务,并在它们全部完成后进行join操作等待结果,那么这个代码的执行时间就取决于最长的业务执行的时间。
import com.example.demo.entity.Customer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {
/**
* 任务1
*/
static String getName() {
try {
// 停止执行1秒钟
TimeUnit.SECONDS.sleep(1); // 停止执行1秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
return "name";
}
/**
* 任务2
*/
static String getScore() {
try {
// 停止执行2秒钟
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "score";
}
/**
* 任务3
*/
static String getOrderInfo() {
try {
// 停止执行3秒钟
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "orderInfo";
}
@GetMapping("/getAllInfoV2")
public Boolean getAllInfoV2() {
// 1、模拟使用线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(30));
StopWatch stopWatch = new StopWatch();
stopWatch.start("多线程并发执行");
// 2、定义任务
Customer customer = new Customer();
// 任务1
CompletableFuture<Boolean> completableFutureGetName = CompletableFuture.supplyAsync(() -> {
customer.setName(getName());
return Boolean.TRUE;
}, threadPoolExecutor);
// 任务2
CompletableFuture<Boolean> completableFutureGetOrderInfo = CompletableFuture.supplyAsync(() -> {
customer.setOrderInfo(getOrderInfo());
return Boolean.TRUE;
}, threadPoolExecutor);
// 任务3
CompletableFuture<Boolean> completableFutureGetScore = CompletableFuture.supplyAsync(() -> {
customer.setScore(getScore());
return Boolean.TRUE;
}, threadPoolExecutor);
// 3、并行执行等待所有任务完成
CompletableFuture.allOf(completableFutureGetName, completableFutureGetOrderInfo, completableFutureGetScore).join();
stopWatch.stop();
log.info("TaskName: " + stopWatch.getLastTaskName() + " --》 耗时(单位:秒):" + stopWatch.getTotalTimeSeconds() + ",耗时(单位:毫秒):" + stopWatch.getTotalTimeMillis());
return Boolean.TRUE;
}
}
调用接口进行测试,业务代码执行了3秒。
三、继续优化:使用动态线程池
上面的方式虽然使用了多线程以及线程池,但是不能实时调整线程池配置信息,对此我们可以尝试通过nacos实现一个动态线程池。
提前说明:采用nacos作为配置中心,nacos服务端的版本为v2.1.2(此处我是采用Docker进行搭建服务端),nacos客户端的版本也是2.1.2版本。
3.1 引入依赖
在pom.xml中引入下述依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.bc</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- 整合Spring Boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 整合Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 整合Spring Cloud Alibaba -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.2 nacos中创建配置
在配置管理的local命名空间下新增配置记录dtp_config,如下图所示:
nacos中初始自定义的线程池配置信息如下:
core:
size : 5
max:
size : 10
3.3 项目配置文件
在resources目录下新建一个名为application.yml的文件:
server:
port: 10086
spring:
application:
name: dtp-server
接着还需在resources目录下新建一个名为bootstrap.yml的文件(bootstrap.yml文件加载顺序先于application.yml):
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:28999
namespace: 322d6d5e-16bb-4f0d-846a-bbb1012eff92
config:
server-addr: 127.0.0.1:28999
namespace: 322d6d5e-16bb-4f0d-846a-bbb1012eff92
group: DTP_SERVER_GROUP # 配置组(如果不指定,则默认为DEFAULT_GROUP)
prefix: dtp_config # Data ID的前缀(如果不指定,则默认取 ${spring.appliction.name})
file-extension: yaml # 指定文件后缀(如果不指定,则默认为properties),此处指定为yaml格式
extension-configs:
- dataId: dtp_config.yaml
group: DTP_SERVER_GROUP
refresh: true # 必须配置,负责自动刷新不生效
refresh-enabled: true # 如果在Nacos控制台界面中人工调整配置项的值,SpringBoot会立即自动取得最新值。因为Nacos客户端带自动刷新功能,可以通过配置 spring.cloud.nacos.config.refresh.enabled=false 来关闭自动刷新
此处虽然配置了服务注册与发现,但其实暂时用不到,这个可以忽略掉。
3.4 核心配置类代码
基于Nacos配置的动态线程池管理功能,可以根据配置的变化来动态调整线程池的参数,同时监控线程池的状态并动态添加任务到线程池中。
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.nacos.api.config.listener.Listener;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@RefreshScope
@Configuration
public class DynamicThreadPool implements InitializingBean {
@Value("${core.size}")
private String coreSize;
@Value("${max.size}")
private String maxSize;
private static ThreadPoolExecutor threadPoolExecutor;
@Autowired
private NacosConfigManager nacosConfigManager;
@Autowired
private NacosConfigProperties nacosConfigProperties;
@Override
public void afterPropertiesSet() throws Exception {
//按照nacos配置初始化线程池
threadPoolExecutor = new ThreadPoolExecutor(Integer.parseInt(coreSize), Integer.parseInt(maxSize), 10L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
new ThreadFactoryBuilder().setNameFormat("c_t_%d").build(),
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("rejected!");
}
});
//nacos配置变更监听,dtp_config是你添加参数配置文件
nacosConfigManager.getConfigService().addListener("dtp_config", nacosConfigProperties.getGroup(),
new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
//配置变更,修改线程池配置
System.out.println(configInfo);
changeThreadPoolConfig(Integer.parseInt(coreSize), Integer.parseInt(maxSize));
}
});
}
/**
* 打印当前线程池的状态
*/
public String printThreadPoolStatus() {
return String.format("core_size:%s,thread_current_size:%s;" +
"thread_max_size:%s;queue_current_size:%s,total_task_count:%s", threadPoolExecutor.getCorePoolSize(),
threadPoolExecutor.getActiveCount(), threadPoolExecutor.getMaximumPoolSize(), threadPoolExecutor.getQueue().size(),
threadPoolExecutor.getTaskCount());
}
/**
* 给线程池增加任务
*
* @param count
*/
public void dynamicThreadPoolAddTask(int count) {
for (int i = 0; i < count; i++) {
final int index = i;
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
/**
* 修改线程池核心参数
*
* @param coreSize
* @param maxSize
*/
private void changeThreadPoolConfig(int coreSize, int maxSize) {
threadPoolExecutor.setCorePoolSize(coreSize);
threadPoolExecutor.setMaximumPoolSize(maxSize);
}
}
另外提醒一下哈,如果代码报错,可能需要添加下述依赖,哈哈哈,暂时我是没有遇到过:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
代码解释:
@RefreshScope:这个注解用来支持nacos的动态刷新功能。
@Value("${max.size}")、@Value("${core.size}"):这两个注解用来读取我们上一步在nacos配置的具体信息,同时在nacos配置变更时,能够实时读取到变更后的内容。
- DynamicThreadPool类实现了InitializingBean接口,意味着在Bean的属性设置后会执行afterPropertiesSet方法,在该方法中完成了线程池的初始化和配置监听。
- 在afterPropertiesSet方法中:创建了一个ThreadPoolExecutor对象 threadPoolExecutor,根据从Nacos配置中获取的core.size和max.size参数来初始化线程池的核心线程数和最大线程数。设置了线程池的队列、线程工厂和拒绝策略。添加了一个 Nacos 配置监听器nacosConfigManager.getConfigService().addListener,用于监听配置文件的变化。当配置信息发生变化时,会通过回调函数 receiveConfigInfo 来更新线程池的配置。
- printThreadPoolStatus():这个方法用于打印当前线程池的状态,返回一个包含线程池各项状态信息的字符串,包括核心线程数、当前活动线程数、最大线程数、队列中等待的任务数以及总任务数量。
- dynamicThreadPoolAddTask(int count):这个方法用于向线程池动态添加任务。根据传入的任务数量 count,循环添加指定数量的任务到线程池中。每个任务都是一个实现了 Runnable 接口的匿名内部类,在其中执行任务逻辑,这里是输出当前任务的编号并休眠10秒。
- changeThreadPoolConfig(int coreSize, int maxSize):这个方法用于修改线程池的核心参数,包括核心线程数和最大线程数。通过调用 setCorePoolSize() 和 setMaximumPoolSize() 方法,可以动态地修改线程池的核心线程数和最大线程数。
3.5 增加Controller测试类
import com.example.demo.config.DynamicThreadPool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/threadpool")
public class ThreadPoolController {
@Autowired
private DynamicThreadPool dynamicThreadPool;
/**
* 打印当前线程池的状态
*/
@GetMapping("/print")
public String printThreadPoolStatus() {
return dynamicThreadPool.printThreadPoolStatus();
}
/**
* 给线程池增加任务
*
* @param count
*/
@GetMapping("/add")
public String dynamicThreadPoolAddTask(int count) {
dynamicThreadPool.dynamicThreadPoolAddTask(count);
return String.valueOf(count);
}
}
3.6 测试
调用下述接口查看当前的核心线程数和最大线程数:
http://127.0.0.1:10086/threadpool/print
手动更改nacos中配置文件,将其核心线程数修改为6,可以看到控制台会打印相关的线程数信息。通过接口进行查询验证,可以看到更改成功。
我们通过以下接口给服务添加线程数量:
http://127.0.0.1:10086/threadpool/add?count=15
可以看到当最大线程数满了的时候就开始执行拒绝策略:
rejected!
rejected!
rejected!
rejected!
rejected!
1
9
8
3
6
7
4
5
2
0
这时候我们修改线程池参数修改最大线程数是200,再次调用上述添加任务接口,可以看到没有拒绝信息。
3.7 线程数设置参考策略
对于IO密集型任务和计算密集型任务,线程池的设置略有不同:
IO密集型任务:通常建议设置较大的线程池大小,以便充分利用CPU等资源,同时能够处理大量的IO操作。可以考虑设置线程池大小为2*CPU核心数或更大,这样可以充分利用系统资源并提高IO操作的并发处理能力。
计算密集型任务:由于任务主要耗费在CPU计算上,因此需要限制线程池的大小,避免过多线程竞争CPU资源而导致性能下降。建议将线程池的大小设置为CPU核心数加1或2,这样可以充分利用CPU资源而又不至于引起过多的线程切换导致性能损失。