在工作中,我們肯定遇到過一個介面要處理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.