自定义异步线程的编程使用

317 阅读3分钟

背景

最近项目中要使用到发邮件的功能,要求不影响主线程的执行,因此准备了一个自定义的异步线程去尝试执行。代码和测试过程分享给大家。

代码

工具类的线程池定义

核心线程数:以cpu的物理线程数为准,通过Runtime.getRuntime().availableProcessors();来获取,获得的数字即是我们口中常说的4核8线程、6核12线程的线程数。

最大线程数:因为我们项目需要异步处理的是发送邮件功能,邮件中带有附件,需要用到大量的IO传输,属于IO密集型任务,因此,这里将最大线程数设置为核心线程数的两倍。

IO密集型任务:以磁盘读取或网络传输为主的任务一般被称为IO密集型任务,一个线程处于IO等待时,其他线程仍可使用CPU,如果线程设置为CPU的物理线程数(例如12线程),则最多也就允许12条IO任务,但实际上这个时候的CPU是空闲的,完全可以在IO等待的时间去做其他事情,因此最大线程数可以设置的大一点,让CPU在不同的线程之间切换,这样当每个线程都处于IO等待时,就相当于所有线程同步执行。这个参数一般根据业务定,但一定要大于物理线程数,否则就是浪费性能。

CPU密集型任务:以计算为主,例如存在大量的JSON转换的任务,相对于IO密集型任务被称作CPU密集型任务,两个概念是相对的,根据业务场景决定。因为线程设计大量计算,需要占用大量的CPU时间片,如果CPU开启的线程超过了物理线程,那么CPU就会不断在各个线程之间进行切换,而线程切换也是存在资源消耗的,频繁切换反而降低了CPU的性能利用率,因此如果是CPU密集型的任务场景,线程池最大线程数一般设置与核心线程数相同,或核心线程数+1。

空闲线程存活时间:1分钟。

任务队列:数组型队列。

拒绝策略:默认的,超出限制直接丢弃任务并抛出异常。

package com.bandao.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.*;
import java.util.function.Supplier;

/**
 * 异步线程工具类
 *
 * @author bandao
 * @date 2021/10/30
 */
public class AsyncThreadUtil {

    /**
     * cpu物理线程数
     */
    private static final int AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();

    /**
     * 线程池
     * 核心线程数:cpu物理线程数
     * 最大线程数:cpu物理线程数
     * 空闲线程存活时间:1分钟
     * 任务队列:数组型队列
     * 拒绝策略:默认的,超出限制直接丢弃任务并抛出异常
     */
    private static final ThreadPoolExecutor IO_EXECUTOR = new ThreadPoolExecutor(AVAILABLE_PROCESSORS,
            AVAILABLE_PROCESSORS * 2,
            1, TimeUnit.MINUTES,
            new ArrayBlockingQueue<>(1024),
            new ThreadPoolExecutor.AbortPolicy());

    /**
     * 日志记录
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(AsyncThreadUtil.class);

    /**
     * 开启异步线程执行任务
     *
     * @param supplier 需要异步执行的方法
     */
    public static <T> void execute(Supplier<T> supplier) {
        IO_EXECUTOR.execute(() -> {
            T result = supplier.get();
            //日志记录执行结果
            LOGGER.info(String.format("execute result:%s", result));
        });
    }
}

测试

模拟IO读存代码

在传输前后会计算当前线程传输所用的时间,我们对比的主要是主线程的执行时间。

package com.bandao.async;

import java.io.*;

/**
 * 任务
 *
 * @author bandao
 * @date 2021/10/30
 */
public class Task {
    /**
     * 模拟任务
     */
    public static boolean doSomething() {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            //1.指向数据源路径
            bis = new BufferedInputStream(new FileInputStream("D:/test.zip"));
            //2.指向目的地路径
            bos = new BufferedOutputStream(new FileOutputStream("files/test.zip"));
            System.out.println(">>>文件传输开始<<<");
            long start = System.currentTimeMillis();
            //3.一次读写缓冲区一个字节数组
            byte[] bytes = new byte[1024];
            int len;
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
                bos.flush();
            }
            System.out.printf("传输用时 %d ms%n", System.currentTimeMillis() - start);
            System.out.println(">>>文件传输完毕<<<");
            return true;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        } finally {
            //4.释放资源
            try {
                assert bis != null;
                bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                assert bos != null;
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

主线程同步调用

package com.bandao.async;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 异步演示
 *
 * @author bandao
 * @date 2021/10/30
 */
public class AsyncDemo {

    /**
     * 日志记录
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(AsyncDemo.class);

    public static void main(String[] args) {
        //1.设置开始时间
        System.out.println(">>>主线程开始执行<<<");
        long start = System.currentTimeMillis();

        //2.主线程同步调用任务
        Task.doSomething();

        //3.计算执行时间
        long time = System.currentTimeMillis() - start;

        //4.打印执行结果
        LOGGER.info(String.format("主线程耗时:%dms", time));
    }
}

主线程使用当前线程同步调用IO传输任务后,执行结果为:

>>>主线程开始执行<<<
>>>文件传输开始<<<
传输用时 9636 ms
>>>文件传输完毕<<<
2021-10-30 21:28:05 [main] INFO  com.bandao.async.AsyncDemo -主线程耗时:9671ms

可以看到,主线程因为IO的传输被占用,无法执行输出打印,待IO传输结束后,主线程才能执行打印,整个主线程耗时为9671ms。

主线程异步调用

package com.bandao.async;

import com.bandao.utils.AsyncThreadUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 异步演示
 *
 * @author bandao
 * @date 2021/10/30
 */
public class AsyncDemo {

    /**
     * 日志记录
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(AsyncDemo.class);

    public static void main(String[] args) {
        //1.设置开始时间
        System.out.println(">>>主线程开始执行<<<");
        long start = System.currentTimeMillis();

        //2.主线程异步调用任务
        AsyncThreadUtil.execute(Task::doSomething);

        //3.计算执行时间
        long time = System.currentTimeMillis() - start;

        //4.打印执行结果
        LOGGER.info(String.format("主线程耗时:%dms", time));
    }
}

主线程使用当前线程异步调用IO传输任务后,执行结果为:

>>>主线程开始执行<<<
>>>文件传输开始<<<
2021-10-30 21:30:56 [main] INFO  com.bandao.async.AsyncDemo -主线程耗时:2ms
传输用时 9640 ms
>>>文件传输完毕<<<
2021-10-30 21:31:06 [pool-2-thread-1] INFO  com.bandao.utils.AsyncThreadUtil -execute result:true

可以看到,主线程从开始到打印仅耗时2ms,而异步线程[pool-2-thread-1]进行IO传输的过程并没有对主线程造成影响,因此此次测试还是成功的。

其他资源

pom.xml文件分享,主要是配置logback的日志功能

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>groupId</groupId>
    <artifactId>CoreJava3</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.21</version>
        </dependency>
        <!--用于桥接commons-logging 到 slf4j-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.6.2</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.1.7</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.1.7</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
    </dependencies>

</project>

logback.xml文件,从网上复制来的,比较全,可以根据需求更改

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!-- 日志保存路径,可以是绝对路径,也可以是相对路径,
    logback会自动创建文件夹,这样设置了就可以输出日志文件了  -->
    <substitutionProperty name="logbase" value="logs/"/>
    <!-- 这个是要配置输出文件的 -->
    <!-- ConsoleAppender 控制台输出日志 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 对日志进行格式化 -->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger -%msg%n</pattern>
        </encoder>
    </appender>
    <!-- ERROR级别日志 -->
    <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 RollingFileAppender-->
    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 过滤器,只记录WARN级别的日志 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <!-- 最常用的滚动策略,它根据时间来制定滚动策略.既负责滚动也负责出发滚动 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--日志输出位置  可相对、和绝对路径 -->
            <fileNamePattern>${logbase}%d{yyyy-MM-dd}/error%i.log</fileNamePattern>
            <!--日志最多保留30天,单个文件最大20mb,该类型日志文件一共不能超过400mb-->
            <MaxHistory>30</MaxHistory>
            <maxFileSize>20MB</maxFileSize>
            <totalSizeCap>400MB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- INFO级别日志 appender -->
    <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 过滤器,只记录INFO级别的日志 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 按天回滚 daily -->
            <fileNamePattern>${logbase}%d{yyyy-MM-dd}/info%i.log</fileNamePattern>
            <MaxHistory>30</MaxHistory>
            <maxFileSize>20MB</maxFileSize>
            <totalSizeCap>400MB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
        </encoder>
    </appender>


    <!-- DEBUG级别日志 appender -->
    <appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 过滤器,只记录DEBUG级别的日志 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>DEBUG</level>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 按天回滚 daily -->
            <fileNamePattern>${logbase}%d{yyyy-MM-dd}/debug%i.log</fileNamePattern>
            <MaxHistory>30</MaxHistory>
            <maxFileSize>20MB</maxFileSize>
            <totalSizeCap>400MB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- root级别  info将会屏蔽debug级别的日志 -->
    <root level="info">
        <!-- 控制台输出 -->
        <appender-ref ref="STDOUT"/>
        <!-- 文件输出 -->
        <appender-ref ref="ERROR"/>
        <appender-ref ref="INFO"/>
        <appender-ref ref="DEBUG"/>
    </root>
</configuration>