譯注:文本有所精簡和意譯 原文鏈接 : Java Parallel Streams Are Bad for Your Health!原作者:OLEG SHELAJEV 翻譯:Hason 轉載請保留相關信息
Java8 提供了三個我們渴望的重要的功能:Lambdas 、 Stream API、以及接口的默認方法。不過我們很容易濫用它們甚至破壞自己的代碼。
今天我們來看看Stream api,尤其是 parallel streams。這篇文章概述了其中的陷阱。
但是首先讓我們看看Stream api備受稱贊的原因——并行執行。它通過默認的ForkJoinPool,可能提高你的多線程任務的速度。
Parallel Streams 的陷阱以下是一個使用 parallel streams 的似乎很棒的經典的例子(Here’s a classic example of the awesomeness that parallel streams PRomise you)。在這個例子中,我們想同時查詢多個搜索引擎并且獲得第一個返回的結果。
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 result11 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的存在,這樣平行的等待相對于使用主線程的等待會產生的一種副作用?,F在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()在我的代碼里之前我仍然會仔細思考并且再三地審閱包含它的所有代碼。
新聞熱點
疑難解答