Java 中的並行處理

25minutes發表於2021-09-09

1. 背景

本文是一個短文章,介紹Java 中的並行處理。

說明:10多分鐘讀完的文章我稱之為短文章,適合快速閱讀。

2.知識

平行計算(parallel computing)一般是指許多指令得以同時進行的計算模式。在同時進行的前提下,可以將計算的過程分解成小部分,之後以併發方式來加以解決。

也就是分解為幾個過程:

  • 1、將一個大任務拆分成多個子任務,子任務還可以繼續拆分。

  • 2、各個子任務同時進行運算執行。

  • 3、在執行完畢後,可能會有個 " 歸納 " 的任務,比如 求和,求平均等。

再簡化一點的理解就是: 先拆分  -->  在同時進行計算  --> 最後“歸納”

為什麼要“並行”,優點呢?

  • 1、為了獲得 “節省時間”,“快”。適合用於大規模運算的場景。從理論上講,在 n 個並行處理的執行速度可能會是在單一處理機上執行的速度的 n 倍。

  • 2、以前的計算機是單核的,現代的計算機Cpu都是多核的,伺服器甚至都是多Cpu的,平行計算可以充分利用硬體的效能。

3. Java 中的並行處理

JDK 8 新增的Stream API(java.util.stream)將生成環境的函數語言程式設計引入了Java庫中,可以方便開發者能夠寫出更加有效、更加簡潔的程式碼。

steam 的另一個價值是創造性地支援並行處理(parallel processing)。示例:

final Collection tasks = Arrays.asList(

    new Task( Status.OPEN, 5 ),

    new Task( Status.OPEN, 13 ),

    new Task( Status.CLOSED, 8 ) 

);


// 並行執行多個任務,並 求和

final double totalPoints = tasks

   .stream()

   .parallel()

   .map( task -> task.getPoints() ) // or map( Task::getPoints ) 

   .reduce( 0, Integer::sum );

 

System.out.println( "Total points (all tasks): " + totalPoints );



4. 擴充套件

執行緒池方式實現並行處理

jdk1.5引入了併發包,其中包括了ThreadPoolExecutor,相關程式碼如下:

public class ExecutorServiceTest {

 

    public static final int THRESHOLD = 10_000;

    public static long[] numbers;

 

    public static void main(String[] args) throws Exception {

        numbers = LongStream.rangeClosed(1, 10_000_000).toArray();

        ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1);

        CompletionService completionService = new ExecutorCompletionService(executor);

        int taskSize = (int) (numbers.length / THRESHOLD);

        for (int i = 1; i

            final int key = i;

            completionService.submit(new Callable() {

 

                @Override

                public Long call() throws Exception {

                    return sum((key - 1) * THRESHOLD, key * THRESHOLD);

                }

            });

        }

        long sumValue = 0;

        for (int i = 0; i

            sumValue += completionService.take().get();

        }

        // 所有任務已經完成,關閉執行緒池

        System.out.println("sumValue = " + sumValue);

        executor.shutdown();

    }

 

    private static long sum(int start, int end) {

        long sum = 0;

        for (int i = start; i

            sum += numbers[i];

        }

        return sum;

    }

}


public class ExecutorServiceTest {

 

    public static final int THRESHOLD = 10_000;

    public static long[] numbers;

 

    public static void main(String[] args) throws Exception {

        numbers = LongStream.rangeClosed(1, 10_000_000).toArray();

        ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1);

        CompletionService completionService = new ExecutorCompletionService(executor);

        int taskSize = (int) (numbers.length / THRESHOLD);

        for (int i = 1; i

            final int key = i;

            completionService.submit(new Callable() {

 

                @Override

                public Long call() throws Exception {

                    return sum((key - 1) * THRESHOLD, key * THRESHOLD);

                }

            });

        }

        long sumValue = 0;

        for (int i = 0; i

            sumValue += completionService.take().get();

        }

        // 所有任務已經完成,關閉執行緒池

        System.out.println("sumValue = " + sumValue);

        executor.shutdown();

    }

 

    private static long sum(int start, int end) {

        long sum = 0;

        for (int i = start; i

            sum += numbers[i];

        }

        return sum;

    }

}



使用 fork/join框架

分支/合併框架的目的是以遞迴的方式將可以並行的認為拆分成更小的任務,然後將每個子任務的結果合併起來生成整體結果;相關程式碼如下:

public class ForkJoinTest extends java.util.concurrent.RecursiveTask {

    

    private static final long serialVersionUID = 1L;

    private final long[] numbers;

    private final int start;

    private final int end;

    public static final long THRESHOLD = 10_000;

 

    public ForkJoinTest(long[] numbers) {

        this(numbers, 0, numbers.length);

    }

 

    private ForkJoinTest(long[] numbers, int start, int end) {

        this.numbers = numbers;

        this.start = start;

        this.end = end;

    }

 

    @Override

    protected Long compute() {

        int length = end - start;

        if (length

            return computeSequentially();

        }

        ForkJoinTest leftTask = new ForkJoinTest(numbers, start, start + length / 2);

        leftTask.fork();

        ForkJoinTest rightTask = new ForkJoinTest(numbers, start + length / 2, end);

        Long rightResult = rightTask.compute();

        // 注:join方法會阻塞,因此有必要在兩個子任務的計算都開始之後才執行join方法

        Long leftResult = leftTask.join();

        return leftResult + rightResult;

    }

 

    private long computeSequentially() {

        long sum = 0;

        for (int i = start; i

            sum += numbers[i];

        }

        return sum;

    }

 

    public static void main(String[] args) {

        System.out.println(forkJoinSum(10_000_000));

    }

 

    public static long forkJoinSum(long n) {

        long[] numbers = LongStream.rangeClosed(1, n).toArray();

        ForkJoinTask task = new ForkJoinTest(numbers);

        return new ForkJoinPool().invoke(task);

    }

}



上面的程式碼實現了 遞迴方式拆分子任務,並放入到執行緒池中執行。

作者:張雲飛Vir
連結:
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4328/viewspace-2797497/,如需轉載,請註明出處,否則將追究法律責任。

相關文章