【java併發編程】線程返回值

1.場景

​ 假如有個需求,隨機出10個數,打印比x大的數,那我們可以用for循環這10個數,找到比x大的數即可;

​ 那麼如果是隨機100萬個數中打印比x大的數,那這樣我們就可以充分利用多線程的優勢了。

    public static void main(String[] args) {
        final int x=4000;
        // 10個線程一起跑
        for(int y = 0;y<10;y++){
            new Thread(()->{
                Random random = new Random();
                // 循環10萬次
                for(int i =0;i<100000;i++){
                    int num = random.nextInt(1000000);
                    if(num>x){
                        System.out.println(num);
                    }
                }
            }).start();
        }
    }

​ 我們發現10個線程一起跑,每個線程選中1萬個數,這樣的速度一定必單線程快。這種方式可以非常有效的發揮系統的性能;

​ 但是產品大大的需求是千變萬化的,突然產品大大說, 把選中的10萬個數全部加起來 。

​ 或者我的一個實戰場景:人臉庫中有10萬左右個人臉,然後用戶掃臉驗證的時候,多線程比對10萬個人臉的相似度,相似度超過0.8的要收集起來,然後全部線程對比完成之後,把所有超過0.8分數的人臉進行處理,分數最大人臉則認爲是用戶的人臉。

​ 我們知道多線程的start()方法之後是執行了run()方法,但是run()方法是沒有返回值的,如果我們的知識只有這點的話,就很棘手了。

​ 像這種場景就需要我們異步編程來做了。異步編程處理之後,可以在線程執行之後的返回值供我們進行處理。

2.線程返回值

2.1 Callable接口

​ Callable接口和Runable接口及其相似,Callable中call()可以看作爲Runable.run()方法的加強,提供了返回值,並且可以在執行過程中拋出異常。V代表返回值參數類型。

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

2.2 ThreadPoolExecutor 類

​ 由於Callable 有返回值,所以在執行過程衝必須使用一個執行器來使用ThreadPoolExecutor的submit方法來提交任務,get方法獲取任務返回值,如下:

public <T> Future<T> submit(Callable<T> task) ;
// 調用get方法 會一直阻塞,直到任務完成,所以建議使用有等待時間的方法
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;

​ 如果get一直阻塞,可以使用boolean cancel(boolean mayInterruptIfRunning);進行任務取消,mayInterruptIfRunning表示是否允許發送中斷來取消線程,返回值代表是否取消成功。boolean isCancelled();代表是否成功取消,在任務取消之後,如果再次執行get方法會拋異常;

​ boolean isDone();方法返回知否執行完畢,任務執行完畢、任務執行過程中拋出異常、任務取消都會返回true。

ThreadPoolExecutor構造器參數如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
  • corePoolSize 線程池核心大小
  • maximumPoolSize 線程池最大線程數
  • keepAliveTime 和unit 空閒線程存活時間
  • workQueue 隊列類型

2.3 代碼示例

​ 瞭解有返回值的多線程之後,就可以處理上面的場景的,不過爲了測試數量調小一點。

 public static void main(String[] args) {
        final int x=4000;
     // 創建執行器對象,參數下面解釋
        ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 7, 10, TimeUnit.SECONDS, new LinkedBlockingDeque<>(10));
        List<Future<Integer>> list = new ArrayList<>();
     // 創建2個線程
        for(int y = 0;y<2;y++){
            Future<Integer> submit = executor.submit(() -> {
                Random random = new Random();
                Integer sum = 0;
                //  循環2次
                for(int i =0;i<2;i++){
                    int num = random.nextInt(10000);
                    if(num>x){
                        sum=sum+num;
                    }
                }
                System.out.println(sum);
               return sum;
             });
            list.add(submit);
        }
        int sum = 0;
        for(int y=0;y<list.size();y++){
            Future<Integer> integerFuture = list.get(y);
            try {
                sum=sum+integerFuture.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        System.out.println("總和:"+sum);
    }

​ 如上代碼即可完成線程執行完成之後,返回值進行累加。

​ 問題來了,如上代碼比較簡單,線程間執行速度基本一致,但是如果出現線程間速度不一樣的情況就會很大的效率問題。調用get() 方法之後,會一直阻塞,直到當前線程執行完畢爲止,那麼如果線程1線程執行內容比較多,但是線程2執行完畢,主線程也只能等待線程1返回之後再處理線程2。缺陷顯而易見。

3.批量執行異步任務

3.1CompletionService接口

​ Future接口可以實現獲取異步線程的執行結果,但是如果很多異步任務一起處理的話,實現起來會很繁瑣。java.util.concurrent.CompletionService更好的解決了批量任務的處理。

// 提交任務
Future<V> submit(Callable<V> task);
//獲取任務執行結果
Future<V> take() throws InterruptedException;
//獲取任務執行結果 異步
Future<V> poll();
//獲取任務執行結果 異步
Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;

submit方法與ThreadPoolExecutor.submit方法效果類似;不同的是take()方法,CompletionService執行了幾次submit()方法,就可以執行幾次take()方法,如果take()執行的時候沒有執行結束的異步任務,那麼該線程就會被暫停,直到有異步任務執行結束。

ExecutorCompletionService構造器


public ExecutorCompletionService(Executor executor)
public ExecutorCompletionService(Executor executor,
                                     BlockingQueue<Future<V>> completionQueue)
    

通過構造方法很明顯的看出,Executor負責執行異步任務,BlockingQueue負責接收任務執行的結果。

CompletionService接口和ThreadPoolExecutor並不存在繼承關係,只是方法名稱相似而已。ThreadPoolExecutor接口繼承的Executor接口,就一個execute方法,這種抽象使我們不需要關注任務的具體邏輯,達到了解耦的效果。

public interface Executor {
    void execute(Runnable command);
}

3.2代碼實例

優化上面實例中,性能問題。通過take方法,即可做到哪個線程執行速度快先處理哪個線程的返回內容。

 public static void main(String[] args) {
        final int x=4000;
        ExecutorService executorService = Executors.newCachedThreadPool();
        CompletionService completionService = new ExecutorCompletionService(executorService);
        // 創建執行器對象,參數下面解釋
        // 創建2個線程
        for(int y = 0;y<2;y++   ){
             completionService.submit(() -> {
                Random random = new Random();
                Integer sum = 0;
                //  循環2次
                for(int i =0;i<2;i++){
                    int num = random.nextInt(10000);
                    if(num>x){
                        sum=sum+num;
                    }
                }
                System.out.println(sum);
                return sum;
            });
        }
        int sum = 0;
        for(int y=0;y<2;y++){
            Future<Integer> integerFuture = null;
            try {
                //通過take方法,即可做到哪個線程執行速度快先處理哪個線程的返回內容
                integerFuture = completionService.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                sum=sum+integerFuture.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
        System.out.println("總和:"+sum);
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章