简单设计一个JAVA并行处理工具类
在工作中,我们肯定遇到过一个接口要处理N多事项导致接口响应速度很慢的情况,通常我们会综合使用两种方式来提升接口响应速度
- 优化查询SQL,提升查询效率
- 开启多线程并发处理业务数据
这里讨论第二种方案:使用多线程并发处理业务数据,最后处理完成以后,拼装起来返回给前端,每个人的实现方案都不一样,我在工作的这几年也经历了几种写法。
一、几种常见的并行处理写法
方法一:Future写法
其代码形式如下
@Test
public void test1() {
//定义线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
//异步执行
Future<String> getUserName = threadPoolExecutor.submit(() -> {
//do something...
return "kdyzm";
});
//异步执行
Future<Integer> getUserAge = threadPoolExecutor.submit(() -> {
//do something...
return 12;
});
//拼装回调结果
try {
UserInfo user = new UserInfo();
user.setName(getUserName.get());
user.setAge(getUserAge.get());
log.info(JsonUtils.toPrettyString(user));
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
@Data
static class UserInfo {
private String name;
private Integer age;
}
多几个submit一起执行,最后集中get获取最终结果。
这种方式任务一旦多了,就会显得代码很乱,一堆的变量名会让代码可读性很差。
方法二:CompletableFuture.allOf写法
其代码形式如下
@Test
public void test2() {
try {
UserInfo userInfo = new UserInfo();
CompletableFuture.allOf(
//异步执行
CompletableFuture.runAsync(() -> {
userInfo.setName("kdyzm");
}),
//异步执行
CompletableFuture.runAsync(() -> {
userInfo.setAge(12);
})
//同步返回
).get();
log.info(JsonUtils.toPrettyString(userInfo));
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
@Data
static class UserInfo {
private String name;
private Integer age;
}
这种方法使用了CompletableFuture的API,通过将多个异步任务收集起来统一调度最后通过一个get方法同步到主线程。比直接使用Future简化了些。
方法三:CompletableFuture::join写法
其代码形式如下
@Test
public void test3(){
UserInfo userInfo = new UserInfo();
Arrays.asList(
//异步执行
CompletableFuture.supplyAsync(()->{
return "kdyzm";
//回调执行
}).thenAccept(name->{
userInfo.setName(name);
}),
//异步执行
CompletableFuture.supplyAsync(()->{
return 12;
//回调执行
}).thenAccept(age->{
userInfo.setAge(age);
})
//等待所有线程执行完毕
).forEach(CompletableFuture::join);
log.info(JsonUtils.toPrettyString(userInfo));
}
@Data
static class UserInfo {
private String name;
private Integer age;
}
这种写法和上面的写法相比具有更高的可读性,但是它也有缺点:thenAccept只能接收一个返回值,如果想处理多个值,则没有办法,只能使用方法2。
总结
几种写法中第二、三种写法比较常见,使用起来也更加方便,两者各有优缺点:方法2能处理多个返回值,方法3可读性更高。但是无论是方法2还是方法3,它们的使用总是要记住相关的API,使用起来总不是很顺手,可读性虽然方法3更强一些,但是总还是差点意思。此时我就有了自己设计一个简单的并行处理工具类的想法,既要易用,还要可读性高。
二、并行处理工具类设计
1、设计模式选型
因为平时比较喜欢链式调用的API,所以一开始一开始设计,我就想用建造者模式来实现这个工具类。关于建造者模式,详情可以看我之前的文章:设计模式(六):建造者模式 。建造者模式在实际应用中的特点就是链式调用,无论是StringBuilder还是lombok的@Data注解,都使用了建造者模式。
2、第一版代码
仿照方法三,我开发了第一版代码
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* @author kdyzm
*/
@Slf4j
public class ConcurrentWorker {
private List<Task> workers = new ArrayList<>();
public static ConcurrentWorker runner() {
return new ConcurrentWorker();
}
public <R> ConcurrentWorker addTask(Consumer<? super R> action, Supplier<R> value) {
Task<R> worker = new Task<>(action, value);
this.workers.add(worker);
return this;
}
public void run() {
workers.forEach(item -> {
CompletableFuture completableFuture = CompletableFuture.supplyAsync(item.getValue());
item.setCompletableFuture(completableFuture);
});
workers
.stream()
.map(
item -> {
return item.completableFuture.thenAccept(item.getAction());
}
)
.forEach(CompletableFuture::join);
}
@Data
public static class Task<R> {
private Consumer<? super R> action;
private Supplier<R> value;
private CompletableFuture<R> completableFuture;
public Task(Consumer<? super R> action, Supplier<R> value) {
this.action = action;
this.value = value;
}
}
}
这段代码一共不到60行,使用了Lambda表达式和函数式编程相关的API对方法三进行改造,最终使用效果如下
@Test
public void test() {
UserInfo userInfo = new UserInfo();
ConcurrentWorker.runner()
//添加任务
.addTask(userInfo::setName, () -> {
//延迟1000毫秒打印线程执行情况
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread().getName()+"-name");
return "张三";
})
//添加任务
.addTask(userInfo::setAge, () -> {
//延迟1000毫秒打印线程执行情况
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread().getName()+"-age");
return 13;
})
//执行任务
.run();
log.info(JsonUtils.toPrettyString(userInfo));
}
@Data
static class UserInfo {
private String name;
private Integer age;
private String sex;
}
它的使用方式就是
ConcurrentWorker.runner()
.addTask(setter function, return_value function )
.addTask(setter function, return_value function)
.run()
可以看到易用性够了,可读性也很好,但是它的缺点和方法三一样,都只能接收一个参数,毕竟它是根据方法3封装的,接下来改造代码让它支持多参数处理。
3、第二版代码
已知,第一版代码已经支持了如下形式的功能
ConcurrentWorker.runner()
.addTask(setter function, return_value function )
.addTask(setter function, return_value function)
.run()
现在我想添加以下形式的重载方法
.addTask(handle function)
没错,就一个参数,在这个方法中可以任意设置对象值。最终使用的效果如下
@Test
public void test() {
UserInfo userInfo = new UserInfo();
ConcurrentWorker.runner()
.addTask(userInfo::setName, () -> {
try {
Thread.sleep(1000);
log.info(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread().getName()+"-name");
return "张三";
})
.addTask(userInfo::setAge, () -> {
try {
Thread.sleep(1000);
log.info(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread().getName()+"-age");
return 13;
})
//新方法:处理任意多属性值填充
.addTask(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread().getName()+"-sex");
userInfo.setSex("男");
})
.run();
log.info(JsonUtils.toPrettyString(userInfo));
}
@Data
static class UserInfo {
private String name;
private Integer age;
private String sex;
}
完整工具类方法如下
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* @author kdyzm
*/
@Slf4j
public class ConcurrentWorker {
private List<Task> workers = new ArrayList<>();
public static ConcurrentWorker runner() {
return new ConcurrentWorker();
}
public <R> ConcurrentWorker addTask(Consumer<? super R> action, Supplier<R> value) {
Task<R> worker = new Task<>(action, value);
this.workers.add(worker);
return this;
}
public <R> ConcurrentWorker addTask(Runnable runnable) {
Task<R> worker = new Task<>(runnable);
this.workers.add(worker);
return this;
}
public void run() {
workers.forEach(item -> {
int taskType = item.getTaskType();
CompletableFuture completableFuture = null;
switch (taskType) {
case TaskType.RETURN_VALUE:
completableFuture = CompletableFuture.supplyAsync(item.getValue());
break;
case TaskType.VOID_RETURN:
completableFuture = CompletableFuture.runAsync(item.getRunnable());
break;
default:
break;
}
item.setCompletableFuture(completableFuture);
});
workers
.stream()
.map(
item -> {
int taskType = item.getTaskType();
switch (taskType) {
case TaskType.RETURN_VALUE:
return item.completableFuture.thenAccept(item.getAction());
default:
return item.completableFuture.thenAccept(temp->{
//空
});
}
}
)
.forEach(CompletableFuture::join);
}
@Data
public static class Task<R> {
private Consumer<? super R> action;
private Supplier<R> value;
private CompletableFuture<R> completableFuture;
private Runnable runnable;
private int taskType;
public Task(Consumer<? super R> action, Supplier<R> value) {
this.action = action;
this.value = value;
this.taskType = TaskType.RETURN_VALUE;
}
public Task(Runnable runnable) {
this.runnable = runnable;
this.taskType = TaskType.VOID_RETURN;
}
}
public static class TaskType {
/**
* 有返回值的
*/
public static final int RETURN_VALUE = 1;
/**
* 没有返回值的
*/
public static final int VOID_RETURN = 2;
}
}
我将任务类型分为两种,并使用TaskType类封装成常量值:1表示任务执行回调有返回值;2表示任务执行没有返回值,属性填充将在任务执行过程中完成,该类型任务使用Runnable接口实现。
4、工具类jar包
相关代码我已经打包成jar包上传到maven中央仓库,可以通过引入以下maven依赖使用ConcurrentWorker工具类
<dependency>
<groupId>cn.kdyzm</groupId>
<artifactId>kdyzm-util</artifactId>
<version>0.0.2</version>
</dependency>
最后,欢迎关注我的博客:https://blog.kdyzm.cn
END.
简单设计一个JAVA并行处理工具类的更多相关文章
- 简单了解Spring中常用工具类_java - JAVA
文章来源:嗨学网 敏而好学论坛www.piaodoo.com 欢迎大家相互学习 文件资源操作 Spring 定义了一个 org.springframework.core.io.Resource 接口, ...
- 设计一个 Java 程序,自定义异常类,从命令行(键盘)输入一个字符串,如果该字符串值为“XYZ”。。。
设计一个 Java 程序,自定义异常类,从命令行(键盘)输入一个字符串,如果该字符串值为“XYZ”,则抛出一个异常信息“This is a XYZ”,如果从命令行输入 ABC,则没有抛出异常.(只有 ...
- 基于AQS实现的Java并发工具类
本文主要介绍一下基于AQS实现的Java并发工具类的作用,然后简单谈一下该工具类的实现原理.其实都是AQS的相关知识,只不过在AQS上包装了一下而已.本文也是基于您在有AQS的相关知识基础上,进行讲解 ...
- Java Properties工具类详解
1.Java Properties工具类位于java.util.Properties,该工具类的使用极其简单方便.首先该类是继承自 Hashtable<Object,Object> 这就奠 ...
- java日期工具类DateUtil-续二
该版本是一次较大的升级,农历相比公历复杂太多(真佩服古人的智慧),虽然有规律,但涉及到的取舍.近似的感念太多,况且本身的概念就已经很多了,我在网上也是查阅了很多的资料,虽然找到一些计算的方法,但都有些 ...
- 25.大白话说java并发工具类-CountDownLatch,CyclicBarrier,Semaphore,Exchanger
1. 倒计时器CountDownLatch 在多线程协作完成业务功能时,有时候需要等待其他多个线程完成任务之后,主线程才能继续往下执行业务功能,在这种的业务场景下,通常可以使用Thread类的join ...
- JavaEE-实验一 Java常用工具类编程
该博客仅专为我的小伙伴提供参考而附加,没空加上代码具体解析,望各位谅解 1. 使用类String类的分割split 将字符串 “Solutions to selected exercises ca ...
- Android 分享一个SharedPreferences的工具类,方便保存数据
我们平常保存一些数据,都会用到SharedPreferences,他是保存在手机里面的,具体路径是data/data/你的包名/shared_prefs/保存的文件名.xml, SharedPrefe ...
- 用C++设计一个不能被继承的类(用私有构造函数+友元函数)
题目:用C++设计一个不能被继承的类. 分析:这是Adobe公司2007年校园招聘的最新笔试题.这道题除了考察应聘者的C++基本功底外,还能考察反应能力,是一道很好的题目. 在Java中定义了关键字f ...
- Java并发工具类 - CountDownLatch
Java并发工具类 - CountDownLatch 1.简介 CountDownLatch是Java1.5之后引入的Java并发工具类,放在java.util.concurrent包下面 http: ...
随机推荐
- Java面试知识点(三)Java中的单继承和多继承
多继承的优缺点 优点:对象可以调用多个父类中的方法 缺点:如果派生类所继承的多个父类有相同的父类(也就是一个菱形继承结构),而派生类对象需要调用这个祖先类的方法,就会容易出现二义性. 1.java 与 ...
- 【FAQ】HarmonyOS SDK 闭源开放能力 —Map Kit(2)
1.问题描述: 能否设置点击地图,地图标记上的文字不消失? 解决方案: 你好,这个功能设计本身就是点击屏幕marker的信息窗消失:如果用户只是想信息窗中的文字一直展示,可以不用信息窗实现 ,建议可以 ...
- 基于 SQLite 3 的 C 学习:1-开发流程 与 基本函数
背景 SQLite 是 一个 常用于 嵌入式平台的 轻量级的 关系型数据库. 我们已经介绍了 移植 SQLite 3 ,这一讲我们来介绍它的开发,这里仅仅涉及最基本的开发. 高级api:https:/ ...
- ubuntu 安装 arm-none-eabi-gcc 的几种方式
背景 这篇文章主要是为了解决 在 Linux 中 编译 能够在 裸机上 跑的 程序 目前许多嵌入式软件软件开发在Linux平台下进行,编译效率高很多,如今天所述的gcc-arm-none-eabi常适 ...
- KES数据库实践指南:探索KES数据库的事务隔离级别
引言 前两篇文章我们详细讲解了如何安装KES金仓数据库,并提供了快速查询和搭建基于coze平台的智能体的解决方案.今天,我们的焦点将放在并发控制机制和事务隔离级别上. 本文将通过一系列实验操作,深入探 ...
- linux 清理 pyinstaller 打包程序运行留下的临时文件
前言 pyinstaller 打包的 python 二进制可执行程序运行的时候,会在 /tmp 目录下生成 _MEI* (*指的是随机数字)文件夹, 如果程序没有正常退出或者终止了,_MEI* 文件夹 ...
- input标签 手机端数字键盘
要一点击提起数字键盘,安卓只要设置input的类型是number或tel, ios 需要 pattern="number"可以直接打开搜狗输入法的数字键盘,可以输入.和数字如果只能 ...
- SpringBoot中使用Servlet3.0注解开发自定义的拦截器
使用Servlet3.0的注解进行配置步骤 启动类里面加@ServletComponentScan,进行扫描 新建一个Filter类,implements Filter,并实现对应的接口 @WebFi ...
- 算法金 | DL 骚操作扫盲,神经网络设计与选择、参数初始化与优化、学习率调整与正则化、Loss Function、Bad Gradient
大侠幸会,在下全网同名「算法金」 0 基础转 AI 上岸,多个算法赛 Top 「日更万日,让更多人享受智能乐趣」 今日 216/10000 抱个拳,送个礼 神经网络设计与选择 参数初始化与优化 学习率 ...
- vue项目的简单创建与插件下载
准备工作 安装node.js 安装node.js过程全部采用默认配置,一步一步next即可 检验node.js是否安装成功:在cmd命令行中输入node -v以及npm -v 通过cmd创建 安装vu ...