提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
知识点:
线程池线程池: Executors.newSingleThreadExecutor()
多线程线程池: ThreadPoolTaskExecutor
一个可取消的异步线程: FutureTask
一、 需求:
假设:提供一个打印文件的服务:外围控制器负责发起打印请求,服务负责打印数据,打印数据有两种模式,关心打印顺序和不关心打印顺序两种,其中调用方只关心消息指令是否下发成功,不关心执行结果,存在并发 。
需求分析:
1.调用方只关心指令下发是否成功,不关心执行结果:可考虑异步,在接收到指令时立即返回“收到指令”,具体业务的执行过程所需要的时间;
2.并发:使用多线程的思想;
3.方法执行的顺序问题:可从同步执行、或独占线程的角度去考虑;
涉及内容:单线程线程池、多线程线程池、
//一个可取消的异步线程
FutureTask<Boolean> futureTask = new FutureTask<>();//使用 futureTask.get() 会等待线程的结果,将异步的方法变成同步
//单一线程池:只开启一个线程,所有任务排队顺序执行
ExecutorService executor = Executors.newSingleThreadExecutor();
//多线程线程池:同时开启多个线程,所有任务被分配不同线程异步执行
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
二、 解决方案
Java 线程池、同步、异步实践
1.开启一个ApiController,接收外围系统触发打印信息;
2.开启一个线程池,异步处理,触发方法后立马给返回消息;
3.内部执行过程交由主线程去处理,且分别实现线程的顺序执行和异步执行;
三、 基本模块
1.定义线程池配置,处理异步请求
//AsyncConfig.class
/**
* 线程池的配置
* 放入线程池的线程并发执行
*
*@author HeYQ
*@date 2022/5/16
*/
@Configuration
//@EnableAsync
public class AsyncConfig {
//接收报文核心线程数
//@Value("${book.core.poolsize}")//后续可以定义在yml配置文件中
private int bookCorePoolSize = 20;
//接收报文最大线程数
//@Value("${book.max.poolsize}")
private int bookMaxPoolSize = 20;
//接收报文队列容量
//@Value("${book.queue.capacity}")
private int bookQueueCapacity = 20;
//接收报文线程活跃时间(秒)
//@Value("${book.keepAlive.seconds}")
private int bookKeepAliveSeconds = 100;
//接收报文默认线程名称
//@Value("${book.thread.name.prefix}")
private String bookThreadNamePrefix = "print_recv";
/**
* 接口的线程池
*
* @return TaskExecutor taskExecutor接口
* @since JDK 1.8
*/
@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
//newFixedThreadPool
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();//多线程线程池,内部线程是异步执行的
// 设置核心线程数:一般线程池会至少保持这么多的线程数量
executor.setCorePoolSize(bookCorePoolSize);
// 设置最大线程数:线程池最多有这么多的线程数量
executor.setMaxPoolSize(bookMaxPoolSize);
// 设置队列容量
executor.setQueueCapacity(bookQueueCapacity);
// 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(bookKeepAliveSeconds);
// 设置默认线程名称
executor.setThreadNamePrefix(bookThreadNamePrefix);
// 设置拒绝策略
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
//AbortPolicy:用于被拒绝任务的处理程序,它将抛出RejectedExecutionException
//CallerRunsPolicy:用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。
//DiscardOldestPolicy:用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。
//DiscardPolicy:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.initialize();
return executor;
}
}
2.定义单线程池,处理顺序执行的异步请求
//SyncConfig.class
/**
* 单线程池配置
* 放入线程池的线程顺序执行
*
* @author HeYQ
* @date 2022/5/16
*/
@Configuration
public class SyncConfig {
@Bean(name = "singleThreadPool")
public ExecutorService singleThreadPoolExecutor() {
ExecutorService executor = Executors.newSingleThreadExecutor();
return executor;
}
}
3.定义线程,执行具体的工作
//PrintTask.java
/**
* 测试时使用的例子
* 调用该方法,创建一个线程,该现场可以传参,内部可以调用指定的方法
*
* @author HeYQ
* @date 2022/5/16
*/
@Slf4j
@Component
//@Async
public class PrintTask implements Callable<Boolean> {
//操作类型
private String operationType;
public PrintTask() {
}
//增加有参构造方法,使得创建线程时可以传值
public PrintTask(String operationType) {
this.operationType = operationType;
}
@Override
public synchronized Boolean call() throws Exception {
String curId = PrimaryGenerator.getInstance().nextFormNo("");//自己的方法,获取一个主键ID,也可以自己写
int a = 0;
while (a < 100) {
a++;
log.info(this.operationType + curId + "开始执行" + "第" + a + "次");
Thread.sleep(500);
}
return true;
}
}
4.定义接口API,往线程池中创建任务
//忽略我Post传的数据对象,实际只需要能触发就行
@Log4j
@RestController
@RequestMapping(path = "/Print")
public class AsyncPrintController {
@Resource
ThreadPoolTaskExecutor threadPoolTaskExecutor;//多线程池
@Resource
ExecutorService singleThreadPoolExecutor;//单线程池
/**
* 多线程打印:不要求方法执行的顺序
*
* @author HeYQ
*/
@RequestMapping(value = "/asyncPrin", method = RequestMethod.POST)
@ResponseBody
public Result printHtml(@RequestBody LabelInfoDto tempInfo) throws Exception {
try {
Assert.isFalse(tempInfo.getTemplateType().equals(null), "打印失败,条码类型不能为空!");
FutureTask<Boolean> futureTask = new FutureTask<>(new PrintTask("printHtml"));
threadPoolTaskExecutor.submit(futureTask);
return Result.success("接收信息成功");
} catch (Exception ex) {
log.error("打印时出现异常" + ex.getMessage());
return Result.success("接收信息时出现异常:" + ex.getMessage());
}
}
/**
* 单线程打印:关心打印顺序
*
* @author HeYQ
*/
@RequestMapping(value = "/syncPrin", method = RequestMethod.POST)
@ResponseBody
public Result printTest(@RequestBody LabelInfoDto tempInfo) throws Exception {
try {
Assert.isFalse(tempInfo.getTemplateType().equals(null), "打印失败,条码类型不能为空!");
FutureTask<Boolean> futureTask = new FutureTask<>(new PrintTask("printTest"));
singleThreadPoolExecutor.submit(futureTask);
//1.方法不会等待线程执行完,会立马返回 "接收信息成功"
//log.error("拿到结果" + futureTask.get());
//2. futureTask.get() 会等待线程的结果,将异步的方法变成同步
return Result.success("接收信息成功");
} catch (Exception ex) {
log.error("打印测试时出现异常" + ex.getMessage());
return Result.success("时出现异常:" + ex.getMessage());
}
}
}
四、测试结果
1.异步打印:不需要关心打印顺序,会存在交叉打印的情况
调用:
结果:调用了两次,两个线程穿插执行
2.同步打印:接口在调用时就得到返回值,内部方法交由线程池顺序执行,打印连续,不会存在穿插情况
结果:调用了两次,第二次会在第一次完全打印完之后才会执行
总结
以上就是今天要讲的内容,本文仅仅简单介绍了使用线程池实现了异步调用,并通过单线程线程池和多线程线程池的使用,实现了对线程内方法顺序的控制。
补充: FutureTask futureTask = new FutureTask<>(); 的使用。
//1.singleThreadPoolExecutor 为线程池
//2.创建PrintTask线程,定义为可取消的任务
FutureTask<Boolean> futureTask = new FutureTask<>(new PrintTask("printTest"));
//3.将线程交由线程池托管
singleThreadPoolExecutor.submit(futureTask);
//4.方法不会等待线程执行完,会立马返回 "接收信息成功"
//log.error("拿到结果" + futureTask.get());
//5. 如果使用 futureTask.get() 会等待线程的结果,将异步的方法变成同步
return Result.success("接收信息成功");
可参看该片文章:彻底理解Java的Future模式