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

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)爲任務執行結果的取得限制時間,如果沒有完成就取消任務.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章