☕【Java深層系列】「併發程式設計系列」讓我們一起探索一下CompletionService的技術原理和使用指南

浩宇天尚發表於2022-01-26

CompletionService基本介紹

  • CompletionService與ExecutorService類似都可以用來執行執行緒池的任務,ExecutorService繼承了Executor介面,而CompletionService則是一個介面。

  • 主要是Executor的特性決定的,Executor框架不能完全保證任務執行的非同步性,那就是如果需要實現任務(task)的非同步性,只要為每個task建立一個執行緒就實現了任務的非同步性。

在高併發的情況下,不斷建立執行緒非同步執行任務將會極大增大執行緒建立的開銷、造成極大的資源消耗和影響系統的穩定性。另外,Executor框架還支援同步任務的執行,就是在execute方法中呼叫提交任務的run()方法就屬於同步呼叫,當我們採用非同步的時候,需要進行的就是獲取Future物件,之後在需要使用的時候get出來結果即可。

非同步呼叫判斷機制

一般情況下,如果需要判斷任務是否完成,思路是得到Future列表的每個Future,然後反覆呼叫其get方法,並將timeout引數設為0,從而通過輪詢的方式判斷任務是否完成。為了更精確實現任務的非同步執行以及更簡便的完成任務的非同步執行,可以使用CompletionService

CompletionService實現原理

CompletionService實際上可以看做是Executor和BlockingQueue的結合體。CompletionService在接收到要執行的任務時,通過類似BlockingQueue的put和take獲得任務執行的結果。CompletionService的一個實現是ExecutorCompletionService,ExecutorCompletionService把具體的計算任務交給Executor完成。

QueueingFuture的原始碼如下

  • ExecutorCompletionService在建構函式中會建立一個BlockingQueue(使用的基於連結串列的無界佇列LinkedBlockingQueue),該BlockingQueue的作用是儲存Executor執行的結果。當計算完成時,呼叫FutureTask的done方法。

  • 當提交一個任務到ExecutorCompletionService時,首先將任務包裝成QueueingFuture,它是FutureTask的一個子類,然後改寫FutureTask的done方法,之後把Executor執行的計算結果放入BlockingQueue中。

   private class QueueingFuture extends FutureTask<Void> {
       QueueingFuture(RunnableFuture<V> task) {
           super(task, null);
           this.task = task;
       }
       protected void done() { completionQueue.add(task); }
       private final Future<V> task;
   }

CompletionService將提交的任務轉化為QueueingFuture,並且覆蓋了done方法,在done方法中就是將任務加入任務佇列中。

使用ExecutorService實現任務

比如:電商中載入商品詳情這一操作,因為商品屬性的多樣性,將商品的圖片顯示與商品簡介的顯示設為兩個獨立執行的任務。

另外,由於商品的圖片可能有許多張,所以圖片的顯示往往比簡介顯示更慢。這個時候非同步執行能夠在一定程度上加快執行的速度提高系統的效能。

public class DisplayProductInfoWithExecutorService {
    //執行緒池
    private final ExecutorService executorService = Executors.newFixedThreadPool(2);
    //日期格式器
    private final DateFormat format = new SimpleDateFormat("HH:mm:ss");
    // 由於可能商品的圖片可能會有很多張,所以顯示商品的圖片往往會有一定的延遲
    // 除了商品的詳情外還包括商品簡介等資訊的展示,由於這裡資訊主要的是文字為
    // 主,所以能夠比圖片更快顯示出來。下面的程式碼就以執行這兩個任務為主線,完
    // 成這兩個任務的執行。由於這兩個任務的執行存在較大差距,所以想到的第一個
    // 思路就是非同步執行,首先執行影像的下載任務,之後(不會很久)開始執行商品
    // 簡介資訊的展示,如果網路足夠好,圖片又不是很大的情況下,可能在開始展示
    // 商品的時候影像就下載完成了,所以自然想到使用Executor和Callable完成異
    // 步任務的執行。
 
    public void renderProductDetail() {
        final List<ProductInfo>  productInfos = loadProductImages();
        //非同步下載影像的任務
        Callable<List<ProductImage>> task = new Callable<List<ProductImage>>() {
            @Override
            public List<ProductImage> call() throws Exception {
                List<ProductImage> imageList = new ArrayList<>();
                for (ProductInfo info : productInfos){
                    imageList.add(info.getImage());
                }
                return imageList;
            }
        };
        //提交給執行緒池執行
        Future<List<ProductImage>> listFuture = executorService.submit(task);
        //展示商品簡介的資訊
        renderProductText(productInfos);
        try {
            //顯示商品的圖片
            List<ProductImage> imageList = listFuture.get();
            renderProductImage(imageList);
        } catch (InterruptedException e) {
            // 如果顯示圖片發生中斷異常則重新設定執行緒的中斷狀態
            // 這樣做可以讓wait中的執行緒喚醒
            Thread.currentThread().interrupt();
            // 同時取消任務的執行,引數false表示線上程在執行不中斷
            listFuture.cancel(true);
        } catch (ExecutionException e) {
            try {
                throw new Throwable(e.getCause());
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
 
    }
 
    private void renderProductImage(List<ProductImage> imageList ) {
        for (ProductImage image : imageList){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " display products images! "
            + format.format(new Date()));
    }
 
    private void renderProductText(List<ProductInfo> productInfos) {
        for (ProductInfo info : productInfos){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " display products description! "
            + format.format(new Date()));
    }
 
    private List<ProductInfo> loadProductImages() {
        List<ProductInfo> list = new ArrayList<>();
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ProductInfo info = new ProductInfo();
        info.setImage(new ProductImage());
        list.add(info);
        System.out.println(Thread.currentThread().getName() + " load products info! "
                + format.format(new Date()));
        return list;
    }
 
    /**
     * 商品
     */
    private static class ProductInfo{
        private ProductImage image;
 
        public ProductImage getImage() {
            return image;
        }
 
        public void setImage(ProductImage image) {
            this.image = image;
        }
    }
 
    private static class ProductImage{}
 
    public static void main(String[] args){
        DisplayProductInfoWithExecutorService cd = new DisplayProductInfoWithExecutorService();
        cd.renderProductDetail();
        System.exit(0);
    }
}

CompletionService實現任務

使用CompletionService的一大改進就是把多個圖片的載入分發給多個工作單元進行處理,這樣通過分發的方式就縮小了商品圖片的載入與簡介資訊的載入的速度之間的差距,讓這些小任務線上程池中執行,這樣就大大降低了下載所有圖片的時間,所以在這個時候可以認為這兩個任務是同構的。使用CompletionService完成最合適不過了。

public class DisplayProductInfoWithCompletionService {
 
    //執行緒池
    private final ExecutorService executorService;
    //日期格式器
    private final DateFormat format = new SimpleDateFormat("HH:mm:ss");
 
    public DisplayProductInfoWithCompletionService(ExecutorService executorService) {
        this.executorService = executorService;
    }
 
    public void renderProductDetail() {
        final List<ProductInfo> productInfos = loadProductInfos();
        CompletionService<ProductImage> completionService = new ExecutorCompletionService<ProductImage>(executorService);
        //為每個影像的下載建立一個工作任務
        for (final ProductInfo info : productInfos) {
            completionService.submit(new Callable<ProductImage>() {
                @Override
                public ProductImage call() throws Exception {
                    return info.getImage();
                }
            });
        }
        //展示商品簡介的資訊
        renderProductText(productInfos);
        try {
            //顯示商品圖片
            for (int i = 0, n = productInfos.size(); i < n; i++){
                Future<ProductImage> imageFuture = completionService.take();
                ProductImage image = imageFuture.get();
                renderProductImage(image);
            }
        } catch (InterruptedException e) {
            // 如果顯示圖片發生中斷異常則重新設定執行緒的中斷狀態
            // 這樣做可以讓wait中的執行緒喚醒
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            try {
                throw new Throwable(e.getCause());
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
    }
    private void renderProductImage(ProductImage image) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " display products images! "
                + format.format(new Date()));
    }
    private void renderProductText(List<ProductInfo> productInfos) {
        for (ProductInfo info : productInfos) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " display products description! "
                + format.format(new Date()));
    }
    private List<ProductInfo> loadProductInfos() {
        List<ProductInfo> list = new ArrayList<>();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ProductInfo info = new ProductInfo();
        info.setImage(new ProductImage());
        list.add(info);
        System.out.println(Thread.currentThread().getName() + " load products info! "
                + format.format(new Date()));
        return list;
    }
    /**
     * 商品
     */
    private static class ProductInfo {
        private ProductImage image;
 
        public ProductImage getImage() {
            return image;
        }
 
        public void setImage(ProductImage image) {
            this.image = image;
        }
    }
 
    private static class ProductImage {
    }
 
    public static void main(String[] args) {
        DisplayProductInfoWithCompletionService cd = new DisplayProductInfoWithCompletionService(Executors.newCachedThreadPool());
        cd.renderProductDetail();
    }
}

執行結果與上面的一樣。因為多個ExecutorCompletionService可以共享一個Executor,因此可以建立一個特定某個計算的私有的,又能共享公共的Executor的ExecutorCompletionService。

CompletionService解決Future的get方法阻塞問題

解決方法:

CompletionService的take()方法獲取最先執行完的執行緒的Future物件。

測試方法

public static void main(String[] args) throws Exception {
    CallableDemo callable = new CallableDemo(1,100000);
    CallableDemo callable2 = new CallableDemo(1,100);
    ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 5, 5L,TimeUnit.SECONDS, new LinkedBlockingDeque());
    CompletionService csRef = new ExecutorCompletionService(executor);
    System.out.println("main 1 " +System.currentTimeMillis());
    csRef.submit(callable);
    csRef.submit(callable2);
    System.out.println("main 2 " +System.currentTimeMillis());
    System.out.println(csRef.take().get());
    System.out.println("main 3 " +System.currentTimeMillis());
    System.out.println(csRef.take().get());
    System.out.println("main 4 " +System.currentTimeMillis());
}

執行緒類

import java.util.concurrent.Callable;
public class CallableDemo implements Callable<String> {
    private int begin;
    private int end;
    private int sum;
   public CallableDemo(int begin, int end) {
     super();
     this.begin = begin;
     this.end = end;
  }
   public String call() throws Exception {
       for(int i=begin;i<=end;i++){
           for(int j=begin;j<=end;j++){
              sum+=j;
           }
      }
      Thread.sleep(8000);
    return begin+"-" +end+"的和:"+ sum;
   }
}

CompletionService小結

相比ExecutorService,CompletionService可以更精確和簡便地完成非同步任務的執行
CompletionService的一個實現是ExecutorCompletionService,它是Executor和BlockingQueue功能的融合體,Executor完成計算任務,BlockingQueue負責儲存非同步任務的執行結果
在執行大量相互獨立和同構的任務時,可以使用CompletionService
CompletionService可以為任務的執行設定時限,主要是通過BlockingQueue的poll(long time,TimeUnit unit)為任務執行結果的取得限制時間,如果沒有完成就取消任務.

相關文章