對於斐波那契數的計算,咱們都知道最容易理解的就是遞歸的方法: java
public long recursiveFibonacci(int n) {
if (n < 2) {
return 1;
}
return recursiveFibonacci(n - 1) + recursiveFibonacci(n - 2);
}
複製代碼
固然這個遞歸也能夠轉化爲迭代:git
public long iterativeFibonacci(int n) {
long n1 = 1, n2 = 1;
long fi = 2; // n1 + n2
for (int i = 2; i <= n; i++) {
fi = n1 + n2;
n1 = n2;
n2 = fi;
}
return fi;
}
複製代碼
可是,對於以上兩種方法,並不能並行化,由於後一項的值依賴於前一項,使得算法流程是串行的。因此引出了能夠並行的計算斐波那契數的公式: github
=> 面試
f0 和 f1 都是 1 —— 很明顯咱們能夠對 (1, 1; 1, 0) 進行並行計算。算法
首先咱們定義一個 Matrix
類,用來表示一個 2*2
的矩陣:編程
public class Matrix {
/** * 左上角的值 */
public final BigInteger a;
/** * 右上角的值 */
public final BigInteger b;
/** * 左下角的值 */
public final BigInteger c;
/** * 右下角的值 */
public final BigInteger d;
public Matrix(int a, int b, int c, int d) {
this(BigInteger.valueOf(a), BigInteger.valueOf(b),
BigInteger.valueOf(c), BigInteger.valueOf(d));
}
public Matrix(BigInteger a, BigInteger b, BigInteger c, BigInteger d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}
/** * multiply * * @param m multiplier * @return */
public Matrix mul(Matrix m) {
return new Matrix(
a.multiply(m.a).add(b.multiply(m.c)), // a*a + b*c
a.multiply(m.b).add(b.multiply(m.d)), // a*b + b*d
c.multiply(m.a).add(d.multiply(m.c)), // c*a + d*c
c.multiply(m.b).add(d.multiply(m.d)));// c*b + d*d
}
/** * power of exponent * * @param exponent * @return */
public Matrix pow(int exponent) {
Matrix matrix = this.copy();
for (int i = 1; i < exponent; i++) {
matrix = matrix.mul(this);
}
return matrix;
}
public Matrix copy() {
return new Matrix(a, b, c, d);
}
}
複製代碼
而後咱們來比較迭代和並行的效率:segmentfault
咱們先設置並行使用的線程數爲 1,即單線程。微信
public static void main(String[] args) throws Exception {
final int ITEM_NUM = 500000; // 計算斐波那契數列的第 ITEM_NUM 項
System.out.println("開始迭代計算...");
long begin = System.nanoTime();
BigInteger fi1 = iterativeFibonacci(ITEM_NUM);
long end = System.nanoTime();
double time = (end - begin) / 1E9;
System.out.printf("迭代計算用時: %.3f\n\n", time);
/* ------------------------------ */
System.out.println("開始並行計算...");
begin = System.nanoTime();
BigInteger fi2 = parallelFibonacci(ITEM_NUM, 1);
end = System.nanoTime();
time = (end - begin) / 1E9;
System.out.printf("並行計算用時: %.3f\n\n", time);
System.out.println("fi1 == fi2:" + (fi1.equals(fi2)));
}
static BigInteger iterativeFibonacci(int n) {
BigInteger n1 = BigInteger.ONE;
BigInteger n2 = BigInteger.ONE;
BigInteger fi = BigInteger.valueOf(2); // n1 + n2
for (int i = 2; i <= n; i++) {
fi = n1.add(n2);
n1 = n2;
n2 = fi;
}
return fi;
}
static BigInteger parallelFibonacci(int itemNum, int threadNum) throws Exception {
final Matrix matrix = new Matrix(1, 1, 1, 0);
final Matrix primary = new Matrix(1, 0, 1, 0); // (f0, 0; f1, 0)
final int workload = itemNum / threadNum; // 每一個線程要計算的 相乘的項數
// (num / threadNum) 可能存在除不盡的狀況,因此最後一個任務計算全部剩下的項數
final int lastWorkload = itemNum - workload * (threadNum - 1);
List<Callable<Matrix>> tasks = new ArrayList<>(threadNum);
for (int i = 0; i < threadNum; i++) {
if (i < threadNum - 1) {
// 爲了簡潔,使用 Lambda 表達式替代要實現 Callable<Matrix> 的匿名內部類
tasks.add(() -> matrix.pow(workload));
} else {
tasks.add(() -> matrix.pow(lastWorkload));
}
}
ExecutorService threadPool = Executors.newFixedThreadPool(threadNum);
List<Future<Matrix>> futures = threadPool.invokeAll(tasks); // 執行全部任務,invokeAll 會阻塞直到全部任務執行完畢
Matrix result = primary.copy();
for (Future<Matrix> future : futures) { // (matrix ^ n) * (f0, 0; f1, 0)
result = result.mul(future.get());
}
threadPool.shutdown();
return result.c;
}
複製代碼
能夠看到單線程狀況下,使用矩陣運算的效率大概只有迭代計算的 1/3 左右 —— 既然如此,那咱們耍流氓的把並行的線程數改成 10 線程吧:數據結構
BigInteger fi2 = parallelFibonacci(ITEM_NUM, 10); // 10 線程並行計算
複製代碼
能夠看到,此時並行計算的用時碾壓了迭代計算 —— 迭代計算委屈的哭了,並行計算這流氓耍的至關漂亮。併發
好像有點不對勁,我這篇文章的標題彷佛是 使用並行流 —— 並行流呢?
其實前面都是鋪墊 :) 在 parallelFibonacci
方法中,咱們使用了線程池來並行的執行任務,咱們來嘗試將 parallelFibonacci
改成流式(即基於 Stream)風格的代碼:
static BigInteger streamFibonacci(int itemNum, int threadNum) {
final Matrix matrix = new Matrix(1, 1, 1, 0);
final Matrix primary = new Matrix(1, 0, 1, 0);
final int workload = itemNum / threadNum;
final int lastWorkload = itemNum - workload * (threadNum - 1);
// 流式 API
return IntStream.range(0, threadNum) // 產生 [0, threadNum) 區間,用於將任務切分
.parallel() // 使流並行化
.map(i -> i < threadNum - 1 ? workload : lastWorkload)
.mapToObj(w -> matrix.pow(w)) // map -> mN = matrix ^ workload
.reduce((m1, m2) -> m1.mul(m2)) // reduce -> m = m1 * m2 * ... * mN
.map(m -> m.mul(primary)) // map -> m = m * primary
.get().c; // get -> m.c
}
複製代碼
依舊在 10 線程的環境下運行下看看:
public static void main(String[] args) throws Exception {
...
/* ------------------------------ */
System.out.println("開始流式並行計算...");
begin = System.nanoTime();
BigInteger fi3 = streamFibonacci(ITEM_NUM, 10);
end = System.nanoTime();
time = (end - begin) / 1E9;
System.out.printf("流式並行計算用時: %.3f\n\n", time);
System.out.println("fi1 == fi2:" + (fi1.equals(fi2)));
System.out.println("fi1 == fi3:" + (fi1.equals(fi3)));
}
複製代碼
是的,使用並行流就是這麼的簡單,只要你會使用 Stream API
—— 給它加上 .parallel()
—— 它就並行化了。寫了這麼多年的 Java 代碼,從 Java6 到 Java7 再到 Java8,這一刻,我真的感動了(容我擦擦眼淚)。
並且咱們能夠看到,在線程數相同的狀況下,使用 streamFibonacci
(並行流)時,用時要比parallelFibonacci
方法更短。爲了驗證,我誇張一點,將線程數提升到 32:
BigInteger fi2 = parallelFibonacci(ITEM_NUM, 32);
...
BigInteger fi3 = streamFibonacci(ITEM_NUM, 32);
複製代碼
能夠看到,此時 parallelFibonacci
的運行時間反而比 10 線程的時候更長了,而 streamFibonacci
使用的時間卻更短了 —— 流式 API 厲害了!
但這是什麼緣由呢?這個問題留給有興趣的讀者思考和探究吧。
值得注意的是,並行流的底層實現是基於 ForkJoinPool
的,而且使用的是一個共享的 ForkJoinPool —— ForkJoinPool.commonPool()
。爲了充分利用處理器資源和提高程序性能,咱們應該儘可能使用並行流來執行 CPU
密集的任務,而不是 IO
密集的任務 —— 由於共享池中的線程數量是有限的,若是共享池中某些線程執行 IO
密集的任務,那麼這些線程將長時間處於等待 IO
操做完成的狀態,一旦共享池中的線程耗盡,那麼程序中其餘想繼續使用並行流的地方就須要等待,直到有空閒的線程可用,這會在很大程度上影響到程序的性能。因此使用並行流以前,咱們要注意到這個細節。
原文做者:mizhoux 原文地址:segmentfault.com/a/119000000…
大廠筆試內容集合(內有詳細解析) 持續更新中....
歡迎關注我的微信公衆號:Coder編程 歡迎關注Coder編程公衆號,主要分享數據結構與算法、Java相關知識體系、框架知識及原理、Spring全家桶、微服務項目實戰、DevOps實踐之路、每日一篇互聯網大廠面試或筆試題以及PMP項目管理知識等。更多精彩內容正在路上~ 新建了一個qq羣:315211365,歡迎你們進羣交流一塊兒學習。謝謝了!也能夠介紹給身邊有須要的朋友。
文章收錄至 Github: github.com/CoderMerlin… Gitee: gitee.com/573059382/c… 歡迎關注並star~
![]()