1. 程式人生 > >實踐:使用了CompletableFuture之後,程式效能提升了三倍

實踐:使用了CompletableFuture之後,程式效能提升了三倍

CompletableFuture

相比於jdk5所提出的future概念,future在執行的時候支援非同步處理,但是在回撥的過程中依舊是難免會遇到需要等待的情況。

在jdk8裡面,出現了CompletableFuture的新概念,支援對於非同步處理完成任務之後自行處理資料。當發生異常的時候也能按照自定義的邏輯來處理。

如何通過使用CompletableFuture提升查詢的效能呢?

下邊我舉個例子來演示:

首先我們定義一個UserInfo物件:

/**
 * @author idea
 * @data 2020/2/22
 */
public class UserInfo {
    private Integer id;
    private String name;
    private Integer jobId;
    private String jobDes;
    private Integer carId;
    private String carDes;
    private Integer homeId;
    private String homeDes;
    public Integer getId() {
        return id;
    }
    public UserInfo setId(Integer id) {
        this.id = id;
        return this;
    }
    public String getName() {
        return name;
    }
    public UserInfo setName(String name) {
        this.name = name;
        return this;
    }
    public Integer getJobId() {
        return jobId;
    }
    public UserInfo setJobId(Integer jobId) {
        this.jobId = jobId;
        return this;
    }
    public String getJobDes() {
        return jobDes;
    }
    public UserInfo setJobDes(String jobDes) {
        this.jobDes = jobDes;
        return this;
    }
    public Integer getCarId() {
        return carId;
    }
    public UserInfo setCarId(Integer carId) {
        this.carId = carId;
        return this;
    }
    public String getCarDes() {
        return carDes;
    }
    public UserInfo setCarDes(String carDes) {
        this.carDes = carDes;
        return this;
    }
    public Integer getHomeId() {
        return homeId;
    }
    public UserInfo setHomeId(Integer homeId) {
        this.homeId = homeId;
        return this;
    }
    public String getHomeDes() {
        return homeDes;
    }
    public UserInfo setHomeDes(String homeDes) {
        this.homeDes = homeDes;
        return this;
    }
}

 

這個物件裡面的homeid,jobid,carid都是用於匹配對應的住房資訊描述,職業資訊描述,購車資訊描述。

對於將id轉換為描述資訊的方式需要通過額外的sql查詢,這裡做了個簡單的工具類來進行模擬:

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
 * @author idea
 * @data 2020/2/22
 */
public class QueryUtils {
    public String queryCar(Integer carId){
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "car_desc";
    }
    public String queryJob(Integer jobId){
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "job_desc";
    }
    public String queryHome(Integer homeId){
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "home_desc";
    }
}

 

這個工具類的功能看起來會比較通俗易懂,在常規的邏輯裡面,我們做批量物件的轉換大多數都是基於List遍歷,然後在迴圈裡面批量查詢,這樣的方式並非說不行,而是顯得比較過於“暴力”。

假設每次查詢需要消耗1s,那麼遍歷的一個size為n的集合查詢消耗的時間就是n * 3s。

下邊來介紹一種更為方便的技巧:CompletableFuture

定義一個QuerySupplier 實現Supplier介面,根據注入的型別進行轉譯查詢:

import java.util.function.Supplier;
public class QuerySuppiler implements Supplier<String> {
        private Integer id;
        private String type;
        private QueryUtils queryUtils;
        public QuerySuppiler(Integer id, String type,QueryUtils queryUtils) {
            this.id = id;
            this.type = type;
            this.queryUtils=queryUtils;
        }
        @Override
        public String get() {
            if("home".equals(type)){
                return queryUtils.queryHome(id);
            }else if ("job".equals(type)){
                return queryUtils.queryJob(id);
            }else if ("car".equals(type)){
                return queryUtils.queryCar(id);
            }
            return null;
        }
    }

 

由於對應的carid,homeid,jobid都需要到指定的k,v配置表裡面通過核心查詢包裝器來進行轉譯,因此通常的做法就是在for迴圈裡面一個個地進行遍歷解析,這樣的做法也比較易於理解。

QuerySuppiler 是我寫的一個用於做物件解析的服務,程式碼如下所示:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
 * @author idea
 * @data 2020/2/22
 */
public class QueryUserService {
    private Supplier<QueryUtils> queryUtilsSupplier = QueryUtils::new;
    public UserInfo converUserInfo(UserInfo userInfo) {
        QuerySuppiler querySuppiler1 = new QuerySuppiler(userInfo.getCarId(), "car", queryUtilsSupplier.get());
        CompletableFuture<String> getCarDesc = CompletableFuture.supplyAsync(querySuppiler1);
        getCarDesc.thenAccept(new Consumer<String>() {  --1
            @Override
            public void accept(String carDesc) {
                userInfo.setCarDes(carDesc);
            }
        });
        QuerySuppiler querySuppiler2 = new QuerySuppiler(userInfo.getHomeId(), "home", queryUtilsSupplier.get());
        CompletableFuture<String> getHomeDesc = CompletableFuture.supplyAsync(querySuppiler2);
        getHomeDesc.thenAccept(new Consumer<String>() {  --2
            @Override
            public void accept(String homeDesc) {
                userInfo.setHomeDes(homeDesc);
            }
        });
        QuerySuppiler querySuppiler3 = new QuerySuppiler(userInfo.getJobId(), "job", queryUtilsSupplier.get());
        CompletableFuture<String> getJobDesc = CompletableFuture.supplyAsync(querySuppiler3);
        getJobDesc.thenAccept(new Consumer<String>() {  --3
            @Override
            public void accept(String jobDesc) {
                userInfo.setJobDes(jobDesc);
            }
        });
        CompletableFuture<Void> getUserInfo = CompletableFuture.allOf(getCarDesc, getHomeDesc, getJobDesc);
        getUserInfo.thenAccept(new Consumer<Void>() {
            @Override
            public void accept(Void result) {
                System.out.println("全部完成查詢" );
            }
        });
        getUserInfo.join();  --4
        return userInfo;
    }
    public static void main(String[] args) {
        long begin= System.currentTimeMillis();
        //多執行緒環境需要注意執行緒安全問題
        List<UserInfo> userInfoList=Collections.synchronizedList(new ArrayList<>());
        for(int i=0;i<=20;i++){
            UserInfo userInfo=new UserInfo();
            userInfo.setId(i);
            userInfo.setName("username"+i);
            userInfo.setCarId(i);
            userInfo.setJobId(i);
            userInfo.setHomeId(i);
            userInfoList.add(userInfo);
        }
        //stream 查詢一個使用者花費3s  平行計算後一個使用者1秒左右 查詢21個使用者花費21秒
        //parallelStream 速度更慢
        userInfoList.stream()
                .map(userInfo->{
                    QueryUserService queryUserService=new QueryUserService();
                    userInfo =queryUserService.converUserInfo(userInfo);
                    return userInfo;
                }).collect(Collectors.toList());
        System.out.println("=============");
        long end=System.currentTimeMillis();
        System.out.println(end-begin);
    }
}

 

看看這段程式碼的—1,—2,—3部分,三個執行點的位置在使用了thenAccept組裝資料之後,還是可以避開序列化獲取資料的情況。只有在—4的位置才會發生堵塞。這樣對於效能的提升效果更佳。

這裡進行模擬測試,採用原始暴力手段查詢所消耗的時間是20 * 3 =60秒,但是這裡使用了CompletableFuture之後,查詢的時間就會縮短為了21秒。

結果:

全部完成查詢
=============
21223

 

這是一種使用了空間換時間的思路,或許你會說,非同步查詢如果使用FutureTask是不是也可以呢。嗯嗯,是的,但是使用future有個問題,就是在於返回獲取非同步結果的時候需要有等待狀態,這個等待的狀態是需要消耗時間進行堵塞的。

這裡我也做了關於使用普通FutureTask來執行查詢優化的結果:

 /**
     * 使用 FutureTask 來優化查詢
     *
     * @param userInfo
     * @return
     */
    public  UserInfo converUserInfoV2(UserInfo userInfo) {
        Callable<String> homeCallable=new Callable() {
            @Override
            public Object call() throws Exception {
                return queryUtilsSupplier.get().queryHome(userInfo.getHomeId());
            }
        };
        FutureTask<String> getHomeDesc=new FutureTask<>(homeCallable);
        new Thread(getHomeDesc).start();
        futureMap.put("homeCallable",getHomeDesc);
        Callable<String> carCallable=new Callable() {
            @Override
            public Object call() throws Exception {
                return queryUtilsSupplier.get().queryCar(userInfo.getCarId());
            }
        };
        FutureTask<String> getCarDesc=new FutureTask(carCallable);
        new Thread(getCarDesc).start();
        futureMap.put("carCallable",getCarDesc);
        Callable<String> jobCallable=new Callable() {
            @Override
            public Object call() throws Exception {
                return queryUtilsSupplier.get().queryCar(userInfo.getJobId());
            }
        };
        FutureTask<String> getJobDesc=new FutureTask<>(jobCallable);
        new Thread(getJobDesc).start();
        futureMap.put("jobCallable",getJobDesc);
        try {
            userInfo.setHomeDes((String) futureMap.get("homeCallable").get());
            userInfo.setCarDes((String)futureMap.get("carCallable").get());
            userInfo.setJobDes((String)futureMap.get("jobCallable").get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("該物件完成查詢" );
        return userInfo;
    }

 

經過測試,使用 futuretask 進行優化的查詢結果只有47s左右,遠遠不及CompletableFuture的效能高效.這是因為使用了futuretask的get方法依然是存在堵塞的情況。

關鍵部分看這段內容:

userInfo.setHomeDes((String) futureMap.get("homeCallable").get());  --1
userInfo.setCarDes((String)futureMap.get("carCallable").get());  --2
userInfo.setJobDes((String)futureMap.get("jobCallable").get());  --3

 

—1程式碼在執行的時候遇到了堵塞,然後—2和—3的get也需要進行等待,因此使用常規的futuretask進行優化,這裡難免還是會有堵塞的情況。