背景
最近项目中要使用到发邮件的功能,要求不影响主线程的执行,因此准备了一个自定义的异步线程去尝试执行。代码和测试过程分享给大家。
代码
工具类的线程池定义
核心线程数:以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>