Java8中 Parallel Streams 的陷阱 [譯]
轉載自https://www.cnblogs.com/imyijie/p/4478074.html
Java8 提供了三個我們渴望的重要的功能:Lambdas 、 Stream API、以及介面的預設方法。不過我們很容易濫用它們甚至破壞自己的程式碼。
今天我們來看看Stream api,尤其是 parallel streams。這篇文章概述了其中的陷阱。
但是首先讓我們看看Stream api備受稱讚的原因——並行執行。它通過預設的ForkJoinPool,可能提高你的多執行緒任務的速度。
Parallel Streams 的陷阱
以下是一個使用 parallel streams 完美特性的經典例子。在這個例子中,我們想同時查詢多個搜尋引擎並且獲得第一個返回的結果。
1 public static String query(String question) { 2 List<String> engines = new ArrayList<String>() {{ 3 add("http://www.google.com/?q="); 4 add("http://duckduckgo.com/?q="); 5 add("http://www.bing.com/search?q="); 6 }}; 7 // get element as soon as it is available 8 Optional<String> result = engines.stream().parallel().map((base) -> { 9 String url = base + question; 10 // open connection and fetch the result 11 return WS.url(url).get(); 12 }).findAny(); 13 return result.get(); 14 }
是不是很棒?但是讓我們細思深挖一下背後發生了什麼。Parallel streams 被父執行緒執行並且使用JVM預設的 fork join pool: ForkJoinPool.common()
.(關於fork join 上面有連結)
然而,這裡需要注意的一個重要方面是——查詢搜尋引擎是一個阻塞操作。所以在某時刻所有執行緒都會呼叫get()
方法並且在那裡等待結果返回。
等等,這是我們一開始想要的嗎?我們是在同一時間等待所有的結果,而不是遍歷這個列表按順序等待每個回答。
然而,由於ForkJoinPool workders的存在,這樣平行的等待相對於使用主執行緒的等待會產生的一種副作用。現在ForkJoin pool 的實現是:它並不會因為產生了新的workers而抵消掉阻塞的workers。那麼在某個時間所有ForkJoinPool.common()
也就是說,下一次你呼叫這個查詢方法,就可能會在一個時間與其他的parallel stream同時執行,而導致第二個任務的效能大大受損。
不過也不要急著去吐槽ForkJoinPool的實現。在不同的情況下你可以給它一個ManagedBlocker例項並且確保它知道在一個阻塞呼叫中應該什麼時候去移除掉卡住的workers。
現在有意思的一點是,在一個parallel stream處理中並不一定是阻塞呼叫會拖延程式的效能。任何被用於對映在一個集合上的長時間執行的函式都會產生同樣的問題。
看下面這個例子
1 long a = IntStream.range(0, 100).mapToLong(x -> { 2 for (int i = 0; i < 100_000_000; i++) { 3 System.out.println("X:" + i); 4 } 5 return x; 6 }).sum();
這段程式碼同上面那個網路訪問的程式碼遇到了相同的問題。每個lambda的執行並不是瞬間完成的,並且在執行過程中程式中的其他部分將無法訪問這些workers。
這意味著任何依賴parallel streams的程式在什麼別的東西佔用著common ForkJoinPool時將會變得不可預知並且暗藏危機。
那又怎麼樣?我不還是我程式的主人
確實,如果你正在寫一個其他地方都是單執行緒的程式並且準確地知道什麼時候你應該要使用parallel streams,這樣的話你可能會覺得這個問題有一點膚淺。然而,我們很多人是在處理web應用、各種不同的框架以及重量級應用服務。
一個伺服器是怎樣被設計成一個可以支援多種獨立應用的主機的?誰知道呢,給你一個可以並行的卻不能控制輸入的parallel stream?(offer you a predictable parallel stream performance if it doesn’t control the inputs)
一種方式是限制ForkJoinPool提供的並行數。可以通過使用-Djava.util.concurrent.ForkJoinPool.common.parallelism=1 來限制執行緒池的大小為1。不再從並行化中得到好處可以杜絕錯誤的使用它。
另一種方式就是,一個被稱為工作區的可以讓ForkJoinPool平行放置的 parallelStream()
實現。不幸的是現在的JDK還沒有實現。
總結
Parallel streams 是無法預測的,而且想要正確地使用它有些棘手。幾乎任何parallel streams的使用都會影響程式中無關部分的效能,而且是一種無法預測的方式。我毫不懷疑有人能夠設法去正確有效地使用它們。但是在打出stream.parallel()在我的程式碼裡之前我仍然會仔細思考並且再三地審閱包含它的所有程式碼。