使用动态线程池优化你的代码

一、串行化查询

假如任务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配置变更时,能够实时读取到变更后的内容。

  1. DynamicThreadPool类实现了InitializingBean接口,意味着在Bean的属性设置后会执行afterPropertiesSet方法,在该方法中完成了线程池的初始化和配置监听。
  2. 在afterPropertiesSet方法中:创建了一个ThreadPoolExecutor对象 threadPoolExecutor,根据从Nacos配置中获取的core.size和max.size参数来初始化线程池的核心线程数和最大线程数。设置了线程池的队列、线程工厂和拒绝策略。添加了一个 Nacos 配置监听器nacosConfigManager.getConfigService().addListener,用于监听配置文件的变化。当配置信息发生变化时,会通过回调函数 receiveConfigInfo 来更新线程池的配置。
  3. printThreadPoolStatus():这个方法用于打印当前线程池的状态,返回一个包含线程池各项状态信息的字符串,包括核心线程数、当前活动线程数、最大线程数、队列中等待的任务数以及总任务数量。
  4. dynamicThreadPoolAddTask(int count):这个方法用于向线程池动态添加任务。根据传入的任务数量 count,循环添加指定数量的任务到线程池中。每个任务都是一个实现了 Runnable 接口的匿名内部类,在其中执行任务逻辑,这里是输出当前任务的编号并休眠10秒。
  5. 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资源而又不至于引起过多的线程切换导致性能损失。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值