0Pricing
Java Academy · レッスン

よくある落とし穴

順序、ボックス化、共通プールです。

「よくある落とし穴」は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フィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. 並列ストリームの作成
  2. 並列処理が役立つ場合
  3. スレッド安全性と副作用
  4. よくある落とし穴
← Java Academyに戻る