後端思維篇:手把手教你寫一個並行呼叫模板

語言: CN / TW / HK

前言

大家好,我是撿田螺的小男孩。

本文是後端思維專欄的第二篇哈。上一篇36個設計介面的錦囊,得到非常多小夥伴的認可。 36個設計介面的錦囊中也提到一個點:就是使用並行呼叫優化介面。所以接下來就快馬加鞭,寫第二篇:手把手教你寫一個並行呼叫模板。

  • 一個序列呼叫的例子(App首頁資訊查詢)
  • CompletionService實現並行呼叫
  • 抽取通用的並行呼叫方法
  • 程式碼思考以及設計模式應用
  • 思考總結
  • 公眾號:撿田螺的小男孩

1. 一個序列呼叫的例子

如果讓你設計一個APP首頁查詢的介面,它需要查使用者資訊、需要查banner資訊、需要查標籤資訊等等。一般情況,小夥伴會實現如下:

public AppHeadInfoResponse queryAppHeadInfo(AppInfoReq req) { //查使用者資訊 UserInfoParam userInfoParam = buildUserParam(req); UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam); //查banner資訊 BannerParam bannerParam = buildBannerParam(req); BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam); //查標籤資訊 LabelParam labelParam = buildLabelParam(req); LabelDTO labelDTO = labelService.queryLabelInfo(labelParam); //組裝結果 return buildResponse(userInfoDTO,bannerDTO,labelDTO); }

這段程式碼會有什麼問題嘛? 其實這是一段挺正常的程式碼,但是這個方法實現中,查詢使用者、banner、標籤資訊,是序列的,如果查詢使用者資訊200ms,查詢banner資訊100ms,查詢標籤資訊200ms的話,耗時就是500ms啦。

其實為了優化效能,我們可以修改為並行呼叫的方式,耗時可以降為200ms,如下圖所示:

2. CompletionService實現並行呼叫

對於上面的例子,如何實現並行呼叫呢?

有小夥伴說,可以使用Future+Callable實現多個任務的並行呼叫。但是執行緒池執行批量任務時,返回值用Future的get()獲取是阻塞的,如果前一個任務執行比較耗時的話,get()方法會阻塞,形成排隊等待的情況。

CompletionService是對定義ExecutorService進行了包裝,可以一邊生成任務,一邊獲取任務的返回值。讓這兩件事分開執行,任務之間不會互相阻塞,可以獲取最先完成的任務結果。

CompletionService的實現原理比較簡單,底層通過FutureTask+阻塞佇列,實現了任務先完成的話,可優先獲取到。也就是說任務執行結果按照完成的先後順序來排序,先完成可以優化獲取到。內部有一個先進先出的阻塞佇列,用於儲存已經執行完成的Future,你呼叫CompletionService的poll或take方法即可獲取到一個已經執行完成的Future,進而通過呼叫Future介面實現類的get方法獲取最終的結果。

接下來,我們來看下,如何用CompletionService,實現並行查詢APP首頁資訊哈。思考步驟如下:

  1. 我們先把查詢使用者資訊的任務,放到執行緒池,如下: ExecutorService executor = Executors.newFixedThreadPool(10); //查詢使用者資訊 CompletionService<UserInfoDTO> userDTOCompletionService = new ExecutorCompletionService<UserInfoDTO>(executor); Callable<UserInfoDTO> userInfoDTOCallableTask = () -> { UserInfoParam userInfoParam = buildUserParam(req); return userService.queryUserInfo(userInfoParam); }; userDTOCompletionService.submit(userInfoDTOCallableTask);

  2. 但是如果想把查詢banner資訊的任務,也放到這個執行緒池的話,發現不好放了,因為返回型別不一樣,一個是UserInfoDTO,另外一個是BannerDTO。那這時候,我們是不是把泛型宣告為Object即可,因為所有物件都是繼承於Object的?如下:

``` ExecutorService executor = Executors.newFixedThreadPool(10); //查詢使用者資訊 CompletionService baseDTOCompletionService = new ExecutorCompletionService(executor); Callable userInfoDTOCallableTask = () -> { UserInfoParam userInfoParam = buildUserParam(req); return userService.queryUserInfo(userInfoParam); }; //banner資訊任務 Callable bannerDTOCallableTask = () -> { BannerParam bannerParam = buildBannerParam(req); return bannerService.queryBannerInfo(bannerParam); };

//提交使用者資訊任務 baseDTOCompletionService.submit(userInfoDTOCallableTask); //提交banner資訊任務 baseDTOCompletionService.submit(bannerDTOCallableTask); 3. 這裡會有個問題,就是獲取**返回值的時候**,我們不知道哪個Object是使用者資訊的DTO,哪個是BannerDTO```?怎麼辦呢?這時候,我們可以在引數裡面做個擴充套件嘛,即引數宣告為一個基礎物件BaseRspDTO,再搞個泛型放Object資料的,然後基礎物件BaseRspDTO有個區分是UserDTO還是BannerDTO的唯一標記屬性key。程式碼如下:

``` public class BaseRspDTO {

//區分是DTO返回的唯一標記,比如是UserInfoDTO還是BannerDTO
private String key;
//返回的data
private T data;

public String getKey() {
    return key;
}

public void setKey(String key) {
    this.key = key;
}

public T getData() {
    return data;
}

public void setData(T data) {
    this.data = data;
}

}

//並行查詢App首頁資訊 public AppHeadInfoResponse parallelQueryAppHeadPageInfo(AppInfoReq req) {

long beginTime = System.currentTimeMillis();
System.out.println("開始並行查詢app首頁資訊,開始時間:" + beginTime);

ExecutorService executor = Executors.newFixedThreadPool(10);
CompletionService<BaseRspDTO<Object>> baseDTOCompletionService = new ExecutorCompletionService<BaseRspDTO<Object>>(executor);

//查詢使用者資訊任務
Callable<BaseRspDTO<Object>> userInfoDTOCallableTask = () -> {
    UserInfoParam userInfoParam = buildUserParam(req);
    UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam);
    BaseRspDTO<Object> userBaseRspDTO = new BaseRspDTO<Object>();
    userBaseRspDTO.setKey("userInfoDTO");
    userBaseRspDTO.setData(userInfoDTO);
    return userBaseRspDTO;
};

//banner資訊查詢任務
Callable<BaseRspDTO<Object>> bannerDTOCallableTask = () -> {
    BannerParam bannerParam = buildBannerParam(req);
    BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam);
    BaseRspDTO<Object> bannerBaseRspDTO = new BaseRspDTO<Object>();
    bannerBaseRspDTO.setKey("bannerDTO");
    bannerBaseRspDTO.setData(bannerDTO);
    return bannerBaseRspDTO;
};

//label資訊查詢任務
Callable<BaseRspDTO<Object>> labelDTODTOCallableTask = () -> {
    LabelParam labelParam = buildLabelParam(req);
    LabelDTO labelDTO = labelService.queryLabelInfo(labelParam);
    BaseRspDTO<Object> labelBaseRspDTO = new BaseRspDTO<Object>();
    labelBaseRspDTO.setKey("labelDTO");
    labelBaseRspDTO.setData(labelDTO);
    return labelBaseRspDTO;
};

//提交使用者資訊任務
baseDTOCompletionService.submit(userInfoDTOCallableTask);
//提交banner資訊任務
baseDTOCompletionService.submit(bannerDTOCallableTask);
//提交label資訊任務
baseDTOCompletionService.submit(labelDTODTOCallableTask);

UserInfoDTO userInfoDTO = null;
BannerDTO bannerDTO = null;
LabelDTO labelDTO = null;

try {
    //因為提交了3個任務,所以獲取結果次數是3
    for (int i = 0; i < 3; i++) {
        Future<BaseRspDTO<Object>> baseRspDTOFuture = baseDTOCompletionService.poll(1, TimeUnit.SECONDS);
        BaseRspDTO baseRspDTO = baseRspDTOFuture.get();
        if ("userInfoDTO".equals(baseRspDTO.getKey())) {
            userInfoDTO = (UserInfoDTO) baseRspDTO.getData();
        } else if ("bannerDTO".equals(baseRspDTO.getKey())) {
            bannerDTO = (BannerDTO) baseRspDTO.getData();
        } else if ("labelDTO".equals(baseRspDTO.getKey())) {
            labelDTO = (LabelDTO) baseRspDTO.getData();
        }
    }
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}

System.out.println("結束並行查詢app首頁資訊,總耗時:" + (System.currentTimeMillis() - beginTime));
return buildResponse(userInfoDTO, bannerDTO, labelDTO);

} ```

到這裡為止,一個基於CompletionService實現並行呼叫的例子已經實現啦。是不是很開心,哈哈。

3. 抽取通用的並行呼叫方法

我們回過來觀察下第2小節,查詢app首頁資訊的demo:CompletionService實現了並行呼叫。大家有沒有什麼其他想法呢?比如,假設別的業務場景,也想通過並行呼叫優化,那是不是也得搞一套類似第2小節的程式碼。所以,我們是不是可以抽取一個通用的並行方法,讓別的場景也可以用,對吧?這就是後端思維啦

基於第2小節的程式碼,我們如何抽取通用並行呼叫方法呢。

首先,這個通用的並行呼叫方法,不能跟業務相關的屬性掛鉤,對吧,所以方法的入參應該有哪些呢?

方法的入參,可以有Callable對吧。因為並行,肯定是多個Callable任務的。所以,入參應該是一個Callable的陣列。再然後,基於上面的APP首頁查詢的例子,Callable裡面得帶BaseRspDTO泛型,對吧?因此入參就是List<Callable<BaseRspDTO<Object>>> list

那並行呼叫的出參呢? 你有多個Callable的任務,是不是得有多個對應的返回,因此,你的出參可以是List<BaseRspDTO<Object>>。我們抽取的通用並行呼叫模板,就可以寫成醬紫:

``` public List> executeTask(List>> taskList) {

    List<BaseRspDTO<Object>> resultList = new ArrayList<>();
    //校驗引數
    if (taskList == null || taskList.size() == 0) {
        return resultList;
    }

    ExecutorService executor = Executors.newFixedThreadPool(10);
    CompletionService<BaseRspDTO<Object>> baseDTOCompletionService = new ExecutorCompletionService<BaseRspDTO<Object>>(executor);
    //提交任務
    for (Callable<BaseRspDTO<Object>> task : taskList) {
        baseDTOCompletionService.submit(task);
    }

    try {
        //遍歷獲取結果
        for (int i = 0; i < taskList.size(); i++) {
            Future<BaseRspDTO<Object>> baseRspDTOFuture = baseDTOCompletionService.poll(2, TimeUnit.SECONDS);
            resultList.add(baseRspDTOFuture.get());
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }

    return resultList;
}

``` 既然我們是抽取通用的並行呼叫方法,那以上的方法是否還有哪些地方需要改進的呢?

  • 第一個可以優化的地方,就是executor執行緒池,比如有些業務場景想用A執行緒池,有些業務想用B執行緒池,那麼,這個方法,就不通用啦,對吧。我們可以把執行緒池以引數的實行提供出來,給呼叫方自己控制。
  • 第二個可以優化的地方,就是CompletionServicepoll方法獲取時,超時時間是寫死的。因為不同業務場景,超時時間可能不一樣。所以,超時時間也是可以以引數形式放出來,給呼叫方自己控制。

我們再次優化一下這個通用的並行呼叫模板,程式碼如下: ``` public List> executeTask(List>> taskList, long timeOut, ExecutorService executor) {

List<BaseRspDTO<Object>> resultList = new ArrayList<>();
//校驗引數
if (taskList == null || taskList.size() == 0) {
    return resultList;
}
if (executor == null) {
    return resultList;
}
if (timeOut <= 0) {
    return resultList; 
}

//提交任務
CompletionService<BaseRspDTO<Object>> baseDTOCompletionService = new ExecutorCompletionService<BaseRspDTO<Object>>(executor);
for (Callable<BaseRspDTO<Object>> task : taskList) {
    baseDTOCompletionService.submit(task);
}

try {
    //遍歷獲取結果
    for (int i = 0; i < taskList.size(); i++) {
      Future<BaseRspDTO<Object>> baseRspDTOFuture = baseDTOCompletionService.poll(timeOut, TimeUnit.SECONDS);
      resultList.add(baseRspDTOFuture.get());
    }
  } catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}

return resultList;

} ```

以後別的場景也需要用到並行呼叫的話,直接呼叫你的這個方法即可,是不是有點小小的成就感啦,哈哈。

4. 程式碼思考以及設計模式應用

我們把抽取的那個公用的並行呼叫方法,應用到App首頁資訊查詢的例子,程式碼如下:

``` public AppHeadInfoResponse parallelQueryAppHeadPageInfo1(AppInfoReq req) {

    long beginTime = System.currentTimeMillis();
    System.out.println("開始並行查詢app首頁資訊,開始時間:" + beginTime);
    //使用者資訊查詢任務
    Callable<BaseRspDTO<Object>> userInfoDTOCallableTask = () -> {
        UserInfoParam userInfoParam = buildUserParam(req);
        UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam);
        BaseRspDTO<Object> userBaseRspDTO = new BaseRspDTO<Object>();
        userBaseRspDTO.setKey("userInfoDTO");
        userBaseRspDTO.setData(userInfoDTO);
        return userBaseRspDTO;
    };

    //banner資訊查詢任務
    Callable<BaseRspDTO<Object>> bannerDTOCallableTask = () -> {
        BannerParam bannerParam = buildBannerParam(req);
        BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam);
        BaseRspDTO<Object> bannerBaseRspDTO = new BaseRspDTO<Object>();
        bannerBaseRspDTO.setKey("bannerDTO");
        bannerBaseRspDTO.setData(bannerDTO);
        return bannerBaseRspDTO;
    };

    //label資訊查詢任務
    Callable<BaseRspDTO<Object>> labelDTODTOCallableTask = () -> {
        LabelParam labelParam = buildLabelParam(req);
        LabelDTO labelDTO = labelService.queryLabelInfo(labelParam);
        BaseRspDTO<Object> labelBaseRspDTO = new BaseRspDTO<Object>();
        labelBaseRspDTO.setKey("labelDTO");
        labelBaseRspDTO.setData(labelDTO);
        return labelBaseRspDTO;
    };

    List<Callable<BaseRspDTO<Object>>> taskList = new ArrayList<>();
    taskList.add(userInfoDTOCallableTask);
    taskList.add(bannerDTOCallableTask);
    taskList.add(labelDTODTOCallableTask);
    ExecutorService executor = Executors.newFixedThreadPool(10);
    List<BaseRspDTO<Object>> resultList = parallelInvokeCommonService.executeTask(taskList, 3, executor);
    if (resultList == null || resultList.size() == 0) {
        return new AppHeadInfoResponse();
    }

    UserInfoDTO userInfoDTO = null;
    BannerDTO bannerDTO = null;
    LabelDTO labelDTO = null;

    //遍歷結果
    for (int i = 0; i < resultList.size(); i++) {
        BaseRspDTO baseRspDTO = resultList.get(i);
        if ("userInfoDTO".equals(baseRspDTO.getKey())) {
            userInfoDTO = (UserInfoDTO) baseRspDTO.getData();
        } else if ("bannerDTO".equals(baseRspDTO.getKey())) {
            bannerDTO = (BannerDTO) baseRspDTO.getData();
        } else if ("labelDTO".equals(baseRspDTO.getKey())) {
            labelDTO = (LabelDTO) baseRspDTO.getData();
        }
    }

    System.out.println("結束並行查詢app首頁資訊,總耗時:" + (System.currentTimeMillis() - beginTime));
    return buildResponse(userInfoDTO, bannerDTO, labelDTO);
}

```

基於以上程式碼,小夥伴們,是否還有其他方面的優化想法呢? 比如這幾個Callable查詢任務,我們是不是也可以抽取一下?讓程式碼更加簡潔。

二話不說,現在我們直接建一個BaseTaskCommand類,實現Callable介面,把查詢使用者資訊、查詢banner資訊、label標籤資訊的查詢任務放進去。

程式碼如下:

``` public class BaseTaskCommand implements Callable> {

private String key;
private AppInfoReq req;
private IUserService userService;
private IBannerService bannerService;
private ILabelService labelService;

public BaseTaskCommand(String key, AppInfoReq req, IUserService userService, IBannerService bannerService, ILabelService labelService) {
    this.key = key;
    this.req = req;
    this.userService = userService;
    this.bannerService = bannerService;
    this.labelService = labelService;
}

@Override
public BaseRspDTO<Object> call() throws Exception {

    if ("userInfoDTO".equals(key)) {
        UserInfoParam userInfoParam = buildUserParam(req);
        UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam);
        BaseRspDTO<Object> userBaseRspDTO = new BaseRspDTO<Object>();
        userBaseRspDTO.setKey("userInfoDTO");
        userBaseRspDTO.setData(userInfoDTO);
        return userBaseRspDTO;
    } else if ("bannerDTO".equals(key)) {
        BannerParam bannerParam = buildBannerParam(req);
        BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam);
        BaseRspDTO<Object> bannerBaseRspDTO = new BaseRspDTO<Object>();
        bannerBaseRspDTO.setKey("bannerDTO");
        bannerBaseRspDTO.setData(bannerDTO);
        return bannerBaseRspDTO;
    } else if ("labelDTO".equals(key)) {
        LabelParam labelParam = buildLabelParam(req);
        LabelDTO labelDTO = labelService.queryLabelInfo(labelParam);
        BaseRspDTO<Object> labelBaseRspDTO = new BaseRspDTO<Object>();
        labelBaseRspDTO.setKey("labelDTO");
        labelBaseRspDTO.setData(labelDTO);
        return labelBaseRspDTO;
    }

    return null;
}


private UserInfoParam buildUserParam(AppInfoReq req) {
    return new UserInfoParam();
}

private BannerParam buildBannerParam(AppInfoReq req) {
    return new BannerParam();
}

private LabelParam buildLabelParam(AppInfoReq req) {
    return new LabelParam();
}

} 以上這塊程式碼,建構函式還是有**比較多的引數**,並且call()方法中,有多個if...else...,如果新增一個分支(**比如查詢浮層資訊**),那又得在call```方法裡修改了,並且BaseTaskCommand的構造器也要修改了

大家是否有印象,多程式中出現多個if...else...時,我們就可以考慮使用策略模式+工廠模式優化。

我們宣告多個策略實現類,如下:

```

public interface IBaseTask {

//返回每個策略類的key,如
String getTaskType();

BaseRspDTO<Object> execute(AppInfoReq req);

}

//使用者資訊策略類 @Service public class UserInfoStrategyTask implements IBaseTask {

@Autowired
private IUserService userService;

@Override
public String getTaskType() {
    return "userInfoDTO";
}

@Override
public BaseRspDTO<Object> execute(AppInfoReq req) {
    UserInfoParam userInfoParam = userService.buildUserParam(req);
    UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam);
    BaseRspDTO<Object> userBaseRspDTO = new BaseRspDTO<Object>();
    userBaseRspDTO.setKey(getTaskType());
    userBaseRspDTO.setData(userBaseRspDTO);
    return userBaseRspDTO;
}

}

/ * banner資訊策略實現類 / @Service public class BannerStrategyTask implements IBaseTask {

@Autowired
private IBannerService bannerService;

@Override
public String getTaskType() {
    return "bannerDTO";
}

@Override
public BaseRspDTO<Object> execute(AppInfoReq req) {
    BannerParam bannerParam = bannerService.buildBannerParam(req);
    BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam);
    BaseRspDTO<Object> bannerBaseRspDTO = new BaseRspDTO<Object>();
    bannerBaseRspDTO.setKey(getTaskType());
    bannerBaseRspDTO.setData(bannerDTO);
    return bannerBaseRspDTO;
}

}

... 然後這幾個策略實現類,怎麼交給spring管理呢? 我們可以實現ApplicationContextAware```介面,把策略的實現類注入到一個map,然後根據請求方不同的策略請求型別(即DTO的型別),去實現不同的策略類呼叫。其實這類似於工廠模式的思想。程式碼如下:

``` / * 策略工廠類 / @Component public class TaskStrategyFactory implements ApplicationContextAware {

private Map<String, IBaseTask> map = new ConcurrentHashMap<>();

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    Map<String, IBaseTask> tempMap = applicationContext.getBeansOfType(IBaseTask.class);
    tempMap.values().forEach(iBaseTask -> {
        map.put(iBaseTask.getTaskType(), iBaseTask);
    });
}

public BaseRspDTO<Object> executeTask(String key, AppInfoReq req) {
    IBaseTask baseTask = map.get(key);
    if (baseTask != null) {
        System.out.println("工廠策略實現類執行");
        return baseTask.execute(req);
    }
    return null;
}

} ```

有了策略工廠類TaskStrategyFactory,我們再回來優化下BaseTaskCommand類的程式碼。它的構造器已經不需要多個IUserService userService, IBannerService bannerService, ILabelService labelService啦,只需要策略工廠類TaskStrategyFactory即可。同時策略也不需要多個if...else...判斷了,用策略工廠類TaskStrategyFactory代替即可。優化後的程式碼如下:

``` public class BaseTaskCommand implements Callable> {

private String key;
private AppInfoReq req;
private TaskStrategyFactory taskStrategyFactory;

public BaseTaskCommand(String key, AppInfoReq req, TaskStrategyFactory taskStrategyFactory) {
    this.key = key;
    this.req = req;
    this.taskStrategyFactory = taskStrategyFactory;
}

@Override
public BaseRspDTO<Object> call() throws Exception {
    return taskStrategyFactory.executeTask(key, req);
}

}
```

因此整個app首頁資訊並行查詢,就可以優化成這樣啦,如下:

``` public AppHeadInfoResponse parallelQueryAppHeadPageInfo2(AppInfoReq req) { long beginTime = System.currentTimeMillis(); System.out.println("開始並行查詢app首頁資訊(最終版本),開始時間:" + beginTime); List>> taskList = new ArrayList<>(); //使用者資訊查詢任務 taskList.add(new BaseTaskCommand("userInfoDTO", req, taskStrategyFactory)); //banner查詢任務 taskList.add(new BaseTaskCommand("bannerDTO", req, taskStrategyFactory)); //標籤查詢任務 taskList.add(new BaseTaskCommand("labelDTO", req, taskStrategyFactory));

ExecutorService executor = Executors.newFixedThreadPool(10);
List<BaseRspDTO<Object>> resultList = parallelInvokeCommonService.executeTask(taskList, 3, executor);

if (resultList == null || resultList.size() == 0) {
    return new AppHeadInfoResponse();
}

UserInfoDTO userInfoDTO = null;
BannerDTO bannerDTO = null;
LabelDTO labelDTO = null;

for (BaseRspDTO<Object> baseRspDTO : resultList) {
    if ("userInfoDTO".equals(baseRspDTO.getKey())) {
        userInfoDTO = (UserInfoDTO) baseRspDTO.getData();
    } else if ("bannerDTO".equals(baseRspDTO.getKey())) {
        bannerDTO = (BannerDTO) baseRspDTO.getData();
    } else if ("labelDTO".equals(baseRspDTO.getKey())) {
        labelDTO = (LabelDTO) baseRspDTO.getData();
    }
}

System.out.println("結束並行查詢app首頁資訊(最終版本),總耗時:" + (System.currentTimeMillis() - beginTime));
return buildResponse(userInfoDTO, bannerDTO, labelDTO);

} ```

5. 思考總結

以上程式碼整體優化下來,已經很簡潔啦。那還有沒有別的優化思路呢。

其實還是有的,比如,把唯一標記的key定義為列舉,而不是寫死的字串"userInfoDTO"、"bannerDTO","labelDTO"。還有,除了CompletionService,有些小夥伴喜歡用CompletableFuture實行並行呼叫。

本文大家學到了哪些知識呢? 1. 如何優化介面效能?某些場景下,可以使用並行呼叫代替序列。 2. 如何實現並行呼叫呢? 可以使用CompletionService。 3. 學到的後端思維是? 日常開發中,要學會抽取通用的方法、或者工具。 4. 策略模式和工廠模式的應用

本文的話,設計模式這塊還不是很詳細,然後下一篇,給大家講講,我是如何在現有程式碼基礎上,抽取設計模式的哈。然後,如果大家需要本文的完整程式碼的話,可以關注我的公眾號:撿田螺的小男孩,裡面有我的聯絡方式哈。