よくある落とし穴
順序、ボックス化、共通プールです。
「よくある落とし穴」はCoddyKit上の無料Java Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはJava Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Java Academyコースには全4レッスンが含まれています。
並列化は無料の高速化ではない
parallel() を追加すると、コードが速くなるどころか遅くなることがあります。このレッスンでは、避けるべき典型的な落とし穴を扱います。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long sum = IntStream.rangeClosed(1, 1_000_000).parallel()
.asLongStream().sum();
System.out.println(sum);
}
}落とし穴: 順序維持のオーバーヘッド
forEachOrdered や limit のような順序を維持する操作では、スレッド間の調整が必要になり、並列化による効果が薄れます。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
IntStream.rangeClosed(1, 5).parallel()
.forEachOrdered(System.out::println);
}
}unordered が役立つ場合
順序が重要でない場合は、unordered() を呼び出すことで、実行時に順序付けの処理を省略し、より自由に並列化できます。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long count = IntStream.rangeClosed(1, 1_000_000).parallel()
.unordered()
.filter(n -> n % 5 == 0)
.count();
System.out.println(count);
}
}落とし穴: 隠れたボクシング
並列処理で Stream<Integer> を使用すると、スレッド間でボクシングのコストが増大します。数値の並列処理では、プリミティブストリームを優先してください。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long sum = IntStream.rangeClosed(1, 1_000_000).parallel().asLongStream().sum();
System.out.println(sum);
}
}落とし穴: 共有される共通プール
すべての並列ストリームは、1つの ForkJoinPool.commonPool() を共有します。長時間かかる並列タスクが、同じ JVM 内の他のタスクやアプリケーションの別の部分を枯渇させることがあります。
import java.util.concurrent.ForkJoinPool;
public class Main {
public static void main(String[] args) {
System.out.println(ForkJoinPool.commonPool().getParallelism());
}
}カスタムプールで処理を分離する
重い処理を共通プールから切り離すには、独自の ForkJoinPool 内で並列ストリームを送信します。
import java.util.concurrent.ForkJoinPool;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) throws Exception {
ForkJoinPool pool = new ForkJoinPool(2);
long sum = pool.submit(() ->
IntStream.rangeClosed(1, 1_000_000).parallel().asLongStream().sum()
).get();
pool.shutdown();
System.out.println(sum);
}
}落とし穴: 分割しにくいソース
LinkedList、Stream.iterate、BufferedReader.lines は分割が苦手です。これらを並列化しても、効果がほとんどないか、かえって遅くなることがあります。
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
long n = Stream.iterate(1, x -> x + 1)
.limit(100_000)
.parallel()
.filter(x -> x % 2 == 0)
.count();
System.out.println(n);
}
}落とし穴: 状態を持つラムダ式
外部状態を読み書きするラムダ式は、並列実行では非決定的に動作します。一貫した結果を得るには、状態を持たないようにしてください。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
int sum = IntStream.rangeClosed(1, 100).parallel()
.map(n -> n + 1)
.sum();
System.out.println(sum);
}
}落とし穴: findFirst と findAny
findFirst は順序を維持する必要があるため、並列実行ではオーバーヘッドが増えます。一致する要素がどれでもよい場合は、findAny を使用してください。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
int found = IntStream.rangeClosed(1, 1_000_000).parallel()
.filter(n -> n % 99991 == 0)
.findAny()
.getAsInt();
System.out.println(found);
}
}落とし穴: 小さなデータに対する軽い処理
コレクションが非常に小さい場合や処理が単純な場合は、分割と結合のオーバーヘッドが効果を大きく上回ります。逐次実行のままにしてください。
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> small = List.of(1, 2, 3, 4);
int sum = small.stream().mapToInt(Integer::intValue).sum();
System.out.println(sum);
}
}落とし穴: コレクターでの高コストな結合
大規模なソート済み構造の構築など、結合処理のコストが高いコレクターでは、並列化の効果が失われることがあります。単純な蓄積は低コストで結合できます。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long total = IntStream.rangeClosed(1, 1_000_000).parallel()
.mapToLong(n -> n)
.sum();
System.out.println(total);
}
}確認問題
並列ストリームでは、なぜ findFirst が findAny より遅くなることがあるのでしょうか。
まとめ
並列化における一般的な落とし穴を学びました。
- 順序の維持(forEachOrdered、limit、findFirst)には調整コストがかかります。可能であれば
unorderedやfindAnyを使用してください。 - ボクシングによってコストが増大するため、プリミティブストリームを優先してください。
- 共有された共通プールは枯渇することがあるため、重い処理はカスタム
ForkJoinPoolに分離してください。 - 分割しにくいソース、小さなデータ、高コストな結合処理はいずれも並列化の効果を損ないます。
よくある質問
「よくある落とし穴」レッスンは無料ですか?
はい。「よくある落とし穴」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Java Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Java Academyコースには全4レッスンが含まれています。
「よくある落とし穴」で何を学びますか?
順序、ボックス化、共通プールです。 ブラウザで直接実行するハンズオンコードでJava Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Java Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのJava Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。
「よくある落とし穴」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このJava Academyレッスンでコードを書いて実行できますか?
はい。すべてのJava Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- 並列ストリームの作成
- 並列処理が役立つ場合
- スレッド安全性と副作用
- よくある落とし穴