Java Tips | 基本ユーティリティ:CPUコア数取得

Java Java
スポンサーリンク

CPUコア数取得は「どこまで並列化してよいか」を知るための技

マルチスレッドや並列処理を書くとき、「スレッドを何本まで増やしていいのか」はとても重要です。
CPU が 2 コアしかないのに 100 スレッドでガンガン回しても、文句を言うのは CPU ではなく、あなたのシステム利用者です。

そこで使えるのが「CPUコア数取得ユーティリティ」です。
今この JVM が何個のプロセッサ(正確には論理プロセッサ)を使えるのかを知ることで、「スレッドプールのサイズ」「並列ストリームの並列度」などを、環境に合わせて決められるようになります。


基本:Runtime.getRuntime().availableProcessors() で取得する

まずは生の API を触ってみる

CPU コア数(正確には JVM が利用可能なプロセッサ数)は、Runtime クラスから取得できます。

public class CpuBasic {

    public static void main(String[] args) {
        int cores = Runtime.getRuntime().availableProcessors();
        System.out.println("availableProcessors = " + cores);
    }
}
Java

実行すると、例えば 4 や 8 といった数字が出てきます。
ここで返ってくるのは「論理プロセッサ数」であり、物理コア数とは必ずしも一致しません。
ハイパースレッディングが有効な CPU では、「物理 4 コア・論理 8 コア」のような構成もあり、その場合 availableProcessors() は 8 を返します。

ここで押さえておきたい重要ポイントは、「この値は“JVM が見ている世界”のプロセッサ数」であり、「OS 全体の CPU 使用状況」や「他プロセスとの取り合い」までは分からない、ということです。


実務で使える CPUコア数取得ユーティリティの最小形

単純なラッパーでも「意味のある名前」にする

Runtime.getRuntime().availableProcessors() をそのままあちこちに書くと、テストしづらくなったり、意味が伝わりにくくなったりします。
まずは、シンプルでもいいのでユーティリティに閉じ込めてしまいましょう。

public final class CpuCores {

    private CpuCores() {}

    public static int available() {
        return Runtime.getRuntime().availableProcessors();
    }

    public static String summary() {
        return "availableProcessors=" + available();
    }
}
Java

使う側はこう書けます。

System.out.println(CpuCores.summary());
int cores = CpuCores.available();
Java

これだけでも、「ここで CPU コア数を使っている」という意図がコードから読み取りやすくなります。
また、後で「コンテナ環境向けに別の取得方法を使いたい」となったときも、CpuCores の中身だけ差し替えれば済むようになります。


スレッドプールサイズを CPU コア数から決める

固定スレッドプールを「環境に合わせて」作る

よくあるアンチパターンが、固定スレッドプールをこう書いてしまうことです。

ExecutorService pool = Executors.newFixedThreadPool(16);
Java

ローカル開発マシンでは 16 スレッドでも平気かもしれませんが、本番環境が 2 コアの小さなコンテナだった場合、明らかに過剰です。
そこで、「CPU コア数に基づいてスレッド数を決める」ユーティリティを用意します。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public final class ThreadPools {

    private ThreadPools() {}

    public static ExecutorService ioBoundPool() {
        int cores = CpuCores.available();
        int threads = cores * 2; // I/O 待ちが多いならコア数の 2 倍など
        return Executors.newFixedThreadPool(threads);
    }

    public static ExecutorService cpuBoundPool() {
        int cores = CpuCores.available();
        int threads = cores; // CPU 計算中心ならコア数と同じくらい
        return Executors.newFixedThreadPool(threads);
    }
}
Java

使う側はこうです。

ExecutorService pool = ThreadPools.cpuBoundPool();
Java

ここで深掘りしたい重要ポイントは、「CPU バウンドか I/O バウンドかで“適切なスレッド数”が変わる」ということです。

CPU バウンド(計算中心)の処理では、コア数を大きく超えるスレッドを作っても、コンテキストスイッチのオーバーヘッドが増えるだけで逆効果になりがちです。
一方、I/O バウンド(ネットワーク待ち・ディスク待ちが多い)処理では、待ち時間の間 CPU が空くので、コア数より多めのスレッドを用意しても効率が上がることがあります。

「availableProcessors をそのままスレッド数にする」のではなく、「処理の性質を踏まえて係数を決める」感覚を持つことが、実務ではとても大事です。


並列ストリームや ForkJoinPool と CPU コア数

parallelStream の並列度を制御する

Java の parallelStream() は、内部的に ForkJoinPool の「共通プール」を使います。
この共通プールの並列度は、デフォルトでは availableProcessors() に基づいて決まります。

例えば、次のようなコードがあります。

list.parallelStream()
    .map(this::heavyTask)
    .forEach(System.out::println);
Java

このとき、CPU コア数が 8 なら、だいたい 8 前後の並列度で処理されます。
もし「この処理だけ並列度を変えたい」という場合は、専用の ForkJoinPool を作り、CPU コア数から並列度を決めることもできます。

import java.util.concurrent.ForkJoinPool;

ForkJoinPool pool = new ForkJoinPool(CpuCores.available());
pool.submit(() ->
        list.parallelStream()
            .map(this::heavyTask)
            .forEach(System.out::println)
).join();
Java

ここでも、「CPU コア数をベースにしつつ、処理の性質に応じて調整する」という考え方は同じです。


コンテナ環境と availableProcessors の注意点

コンテナの CPU 制限と JVM の見え方

Docker や Kubernetes などのコンテナ環境では、「コンテナに割り当てられた CPU」と「ホスト全体の CPU」が違うことがあります。
古い JVM では、availableProcessors() が「ホスト全体の CPU 数」を返してしまい、コンテナに 1 CPU しか割り当てていないのに 8 などと見えてしまうことがありました。

最近の Java では、コンテナの CPU 制限を認識して availableProcessors() を調整してくれるようになっていますが、
「どのバージョンからどう振る舞うか」は JVM の実装や設定に依存します。

だからこそ、CPU コア数取得をユーティリティに閉じ込めておくと、
「コンテナ環境では別のロジックを使う」「環境変数で上書きできるようにする」といった拡張がしやすくなります。

環境変数で上書きできるようにする例

例えば、「どうしても availableProcessors() の値を信用できない環境がある」場合、
環境変数で上書きできるようにしておくのも一つの手です。

public final class CpuCores {

    private CpuCores() {}

    public static int available() {
        String override = System.getenv("APP_CPU_CORES");
        if (override != null && !override.isBlank()) {
            try {
                return Integer.parseInt(override);
            } catch (NumberFormatException ignored) {
                // 無効なら無視して Runtime の値を使う
            }
        }
        return Runtime.getRuntime().availableProcessors();
    }
}
Java

こうしておけば、特定環境だけ APP_CPU_CORES=2 のように設定して、挙動を調整できます。
「基本は Runtime の値を使うが、必要なら外から上書きできる」という柔らかさを持たせておくと、運用で助かる場面が出てきます。


CPUコア数をログや監視に組み込む

起動時に「このインスタンスのコア数」を記録する

本番で性能問題が起きたとき、「このインスタンスは何コアで動いていたのか」が分からないと、調査が難しくなります。
そこで、アプリ起動時に CPU コア数をログに出しておくのは、実務でよく効くテクニックです。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class CpuLogger {

    private static final Logger log = LoggerFactory.getLogger(CpuLogger.class);

    private CpuLogger() {}

    public static void logCpuInfo() {
        int cores = CpuCores.available();
        log.info("CPU cores available to JVM: {}", cores);
    }
}
Java

起動時に一度だけこう呼びます。

CpuLogger.logCpuInfo();
Java

これで、「どの環境で」「何コアとして認識されていたか」がログから追えるようになります。
メモリ使用量と同じく、「その瞬間の数字」だけでなく、「どんな前提で動いていたか」を記録しておくことが、後から効いてきます。


まとめ:CPUコア数取得ユーティリティで身につけるべき感覚

CPUコア数取得は、「ただ数字を知る」ためではなく、「その数字をもとに並列度やスレッド数を“環境に合わせて”決める」ための道具です。

大事なポイントは、Runtime.getRuntime().availableProcessors() をそのままばらまかず、CpuCores のようなユーティリティに閉じ込めること。
CPU バウンドか I/O バウンドかを意識しながら、「コア数 × 係数」でスレッド数や並列度を決めること。
コンテナ環境などで値が信用しづらい場合に備えて、「環境変数などで上書きできる余地」を設計に残しておくこと。
そして、起動時に CPU コア数をログに残し、「どんな前提で性能問題が起きたのか」を後から追えるようにしておくこと。

タイトルとURLをコピーしました