簡單設計一個JAVA並行處理工具類

狂盗一枝梅發表於2024-08-06

在工作中,我們肯定遇到過一個介面要處理N多事項導致介面響應速度很慢的情況,通常我們會綜合使用兩種方式來提升介面響應速度

  1. 最佳化查詢SQL,提升查詢效率
  2. 開啟多執行緒併發處理業務資料

這裡討論第二種方案:使用多執行緒併發處理業務資料,最後處理完成以後,拼裝起來返回給前端,每個人的實現方案都不一樣,我在工作的這幾年也經歷了幾種寫法。

一、幾種常見的並行處理寫法

方法一: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.

相關文章