1. 程式人生 > 程式設計 >詳解Java8與Runtime.getRuntime().availableProcessors()

詳解Java8與Runtime.getRuntime().availableProcessors()

lambda表示式以及並行流。官方承諾你寫出來的程式碼更執行得更快。流會自動通過Fork/Join池並行地執行。我聽過一些關於Java 8的主題的演講,不過在這個非常關鍵的點上它們都說的有點問題。我計劃在後續的文章中對並行流進行下深入的講解,在這之前我先花點時間仔細地分析下它。關於這個問題,我只想問你們一個非常簡單的問題,不過也是一個非常重要的問題,因為它是很多問題的關鍵所在。這個問題是:

這些並行操作的執行緒都是從哪來的?

在Java 8裡,我們有一個通用的Fork/Join池,我們可以通過ForkJoinPool.commonPool()來訪問它。並行流,並行排序,CompletableFuture等都會用到它。當你構造一個Fork/Join池的時候,通常你都沒有指定最大執行緒數。你只是指定了一個期望的併發數,也就是說你希望在執行時的同一時間有多少活躍的執行緒。當執行緒被阻塞在一個phaser的時候,會建立另一個執行緒來保證池裡有足夠的活躍執行緒。這個phaser就是觸發這個行為的同步器。Fork/Join池最大的執行緒數是32767,但在遠沒達到這個數量時,在大多數作業系統上就會丟擲OutOfMemoryError異常了。在這段示例程式碼中,我會不斷建立新的RecursiveAction真到達到第一個階段(也就是到達了200個執行緒)。如果我們增加到一個更大的數字,比如說到100000,這段程式碼就會失敗了。

import java.util.concurrent.*;

public class PhaserForkJoin {
 public static void main(String... args) {
  ForkJoinPool common = ForkJoinPool.commonPool();
  Phaser phaser = new Phaser(200);
  common.invoke(new PhaserWaiter(phaser));
 }

 private static class PhaserWaiter extends RecursiveAction {
  private final Phaser phaser;

  private PhaserWaiter(Phaser phaser) {
   this.phaser = phaser;
   System.out.println(ForkJoinPool.commonPool().getPoolSize());
  }

  protected void compute() {
   if (phaser.getPhase() > 0) return; // we've passed first phase
   PhaserWaiter p1 = new PhaserWaiter(phaser);
   p1.fork();
   phaser.arriveAndAwaitAdvance();
   p1.join();
  }
 }
}

Fork/Join池沒有一個最大執行緒數,只有一個期望併發數,這是指我們希望同時有多少個活躍執行緒。

通用池是很有用的,因為它意味著不同型別的作業可以共享同一個池,而不用超出程式碼所執行的機器上期望併發數。當然了,如果一個執行緒由於非Phaser的其它原因阻塞了,那可能這個通用池的表現就和預期的不太一樣了。

什麼是通用FJ池的預設的期望併發數?

通常的FJ池的期望併發數的預設值是Runtime.getRuntime().availableProcessors() -1。如果你在一個雙核的機器上通過Arrays.parallelSort()來執行並行排序的話,預設使用的是普通的Arrays.sort()方法。儘管Oracle的官方文件可能許諾你可以獲得性能提升,但是你在一個雙核的機器上可能完全看不著任何提升。

然而,更大的問題在於Runtime.getRuntime().availableProcessors()也並非都能返回你所期望的數值。比如說,在我的雙核1-2-1機器上,它返回的是2,這是對的。不過在我的1-4-2機器 上,也就是一個CPU插槽,4核,每個核2個超執行緒,這樣的話會返回8。不過我其實只有4個核,如果程式碼的瓶頸是在CPU這塊的話,我會有7個執行緒在同時 競爭CPU週期,而不是更合理的4個執行緒。如果我的瓶頸是在記憶體這的話,那這個測試我可以獲得7倍的效能提升。

不過這還沒完!Java Champions上的一個哥們發現了一種情況,他有一臺16-4-2的機器 (也就是16個CPU插槽,每個CPU4個核,每核兩個超執行緒,返回的值居然是16!從我的i7 Macbook pro上的結果來看,我覺得應該返回的是16*4*2=128。在這臺機器上執行Java 8的話,它只會將通用的FJ池的併發數設定成15。正如 Brian Goetz所指出的,“虛擬機器其實不清楚什麼是處理器,它只是去請求作業系統返回一個值。同樣的,作業系統也不知道怎麼回事,它是去問的硬體裝置。硬體會告訴它一個值,通常來說是硬體執行緒數。作業系統相信硬體說的,而虛擬機器又相信作業系統說的。”

所幸的是還有一個解決方案。啟動的時候,你可以通過系統屬性 java.util.concurrent.ForkJoinPool.common.parallelism來設定通用池的併發數。也就是說,我們可以通過-Djava.util.concurrent.ForkJoinPool.common.parallelism=128來啟動這段程式,現在你可以看到它的併發數是128了:

import java.util.concurrent.*;

public class ForkJoinPoolCommon {
 public static void main(String... args) {
  System.out.println(ForkJoinPool.commonPool());
 }
}

還有兩個控制通用池的額外的系統屬性。如果你希望處理未捕獲異常的話,你可以通過java.util.concurrent.ForkJoinPool.common.exceptionHandler來指定一個處理類。如果你希望有自己的執行緒工廠的話,可以通過 java.util.concurrent.ForkJoinPool.common.threadFactory來配置。預設的Fork/Join池的工廠生成的是守護執行緒,可能你的應用裡面不希望使用它。不過如果你這麼做的話請小心——這樣你就無法關閉這個通用池了。

到此這篇關於詳解Java8與Runtime.getRuntime().availableProcessors()的文章就介紹到這了,更多相關Java8與Runtime.getRuntime().availableProcessors()內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!