Javaのスレッドプールの紹介

Javaのスレッドプールの概要

1. 前書き

この記事では、Javaのスレッドプールについて説明します。標準のJavaライブラリのさまざまな実装から始めて、GoogleのGuavaライブラリについて説明します。

2. スレッドプール

Javaでは、スレッドはオペレーティングシステムのリソースであるシステムレベルのスレッドにマップされます。 制御不能にスレッドを作成すると、これらのリソースがすぐに不足する可能性があります。

スレッド間のコンテキスト切り替えは、並列性をエミュレートするために、オペレーティングシステムでも行われます。 単純化したビューでは、生成するスレッドが多いほど、各スレッドが実際の作業に費やす時間が短くなります。

スレッドプールパターンは、マルチスレッドアプリケーションのリソースを節約し、特定の定義済みの制限に並列性を含めるのに役立ちます。

スレッドプールを使用する場合は、write your concurrent code in the form of parallel tasks and submit them for execution to an instance of a thread poolです。 このインスタンスは、これらのタスクを実行するために再利用されるいくつかのスレッドを制御します。 2016-08-10_10-16-52-1024x572

このパターンを使用すると、control the number of threads the application is creatingとそのライフサイクルを実行したり、タスクの実行をスケジュールしたり、受信タスクをキューに保持したりできます。

3. Javaのスレッドプール

3.1. ExecutorsExecutor、およびExecutorService

Executorsヘルパークラスには、事前構成されたスレッドプールインスタンスを作成するためのいくつかのメソッドが含まれています。 これらのクラスは、開始するのに適した場所です。カスタムの微調整を適用する必要がない場合に使用してください。

ExecutorおよびExecutorServiceインターフェースは、Javaのさまざまなスレッドプール実装を操作するために使用されます。 通常、keep your code decoupled from the actual implementation of the thread poolを使用して、アプリケーション全体でこれらのインターフェイスを使用する必要があります。

Executorインターフェースには、実行のためにRunnableインスタンスを送信するための単一のexecuteメソッドがあります。

Executors APIを使用して、タスクを順番に実行するための単一のスレッドプールと無制限のキューに基づくExecutorインスタンスを取得する方法のHere’s a quick example。 ここでは、単に「Hello World」を画面に出力する単一のタスクを実行します。 タスクは、Runnableであると推測されるラムダ(Java 8機能)として送信されます。

Executor executor = Executors.newSingleThreadExecutor();
executor.execute(() -> System.out.println("Hello World"));

ExecutorServiceインターフェースには、controlling the progress of the tasks and managing the termination of the service用の多数のメソッドが含まれています。 このインターフェイスを使用すると、実行するタスクを送信したり、返されたFutureインスタンスを使用してタスクの実行を制御したりできます。

In the following exampleExecutorServiceを作成し、タスクを送信してから、返されたFuturegetメソッドを使用して、送信されたタスクが終了し、値が返されるまで待機します。

ExecutorService executorService = Executors.newFixedThreadPool(10);
Future future = executorService.submit(() -> "Hello World");
// some operations
String result = future.get();

もちろん、実際のシナリオでは、通常、すぐにfuture.get()を呼び出すことは望ましくありませんが、実際に計算の値が必要になるまで、呼び出すのを延期します。

submitメソッドはオーバーロードされ、RunnableまたはCallableのいずれかを取ります。これらは両方とも関数型インターフェースであり、ラムダとして渡すことができます(Java 8以降)。

Runnableの単一メソッドは例外をスローせず、値を返しません。 Callableインターフェースは、例外をスローして値を返すことができるため、より便利な場合があります。

最後に–コンパイラにCallable型を推測させるには、ラムダから値を返すだけです。

ExecutorServiceインターフェースと先物の使用に関するその他の例については、「A Guide to the Java ExecutorService」を参照してください。

3.2. ThreadPoolExecutor

ThreadPoolExecutorは、微調整のための多くのパラメーターとフックを備えた拡張可能なスレッドプールの実装です。

ここで説明する主な構成パラメーターは、corePoolSizemaximumPoolSize、およびkeepAliveTimeです。

プールは、常に内部に保持される一定数のコアスレッドと、生成されて不要になったときに終了する可能性のある過剰なスレッドで構成されます。 corePoolSizeパラメーターは、インスタンス化されてプールに保持されるコアスレッドの量です。 すべてのコアスレッドがビジーで、さらに多くのタスクが送信された場合、プールはmaximumPoolSizeまで拡張できます。

keepAliveTimeパラメータは、過剰なスレッドが発生する時間間隔です(つまり、 corePoolSize)を超えてインスタンス化されたスレッドは、アイドル状態で存在できます。

これらのパラメーターは幅広いユースケースをカバーしますが、the most typical configurations are predefined in the Executors static methodsです。

For examplenewFixedThreadPoolメソッドは、corePoolSizemaximumPoolSizeのパラメーター値が等しく、keepAliveTime.がゼロのThreadPoolExecutorを作成します。これは、のスレッド数がこのスレッドプールは常に同じです。

ThreadPoolExecutor executor =
  (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
executor.submit(() -> {
    Thread.sleep(1000);
    return null;
});
executor.submit(() -> {
    Thread.sleep(1000);
    return null;
});
executor.submit(() -> {
    Thread.sleep(1000);
    return null;
});

assertEquals(2, executor.getPoolSize());
assertEquals(1, executor.getQueue().size());

上記の例では、固定スレッド数2でThreadPoolExecutorをインスタンス化します。 これは、同時に実行されるタスクの量が常に2以下である場合、すぐに実行されることを意味します。 それ以外の場合はsome of these tasks may be put into a queue to wait for their turn

1000ミリ秒スリープすることで重い作業を模倣する3つのCallableタスクを作成しました。 最初の2つのタスクは一度に実行され、3番目のタスクはキューで待機する必要があります。 タスクを送信した直後にgetPoolSize()メソッドとgetQueue().size()メソッドを呼び出すことで確認できます。

別の事前構成されたThreadPoolExecutorは、Executors.newCachedThreadPool()メソッドを使用して作成できます。 このメソッドは、スレッドの数をまったく受け取りません。 このインスタンスでは、corePoolSizeは実際には0に設定され、maximumPoolSizeInteger.MAX_VALUEに設定されます。 keepAliveTimeはこれでは60秒です。

これらのパラメーター値は、the cached thread pool may grow without bounds to accommodate any amount of submitted tasksを意味します。 ただし、スレッドが不要になった場合、非アクティブ状態が60秒続くと破棄されます。 典型的なユースケースは、アプリケーションに多くの短期間のタスクがある場合です。

ThreadPoolExecutor executor =
  (ThreadPoolExecutor) Executors.newCachedThreadPool();
executor.submit(() -> {
    Thread.sleep(1000);
    return null;
});
executor.submit(() -> {
    Thread.sleep(1000);
    return null;
});
executor.submit(() -> {
    Thread.sleep(1000);
    return null;
});

assertEquals(3, executor.getPoolSize());
assertEquals(0, executor.getQueue().size());

上記の例のキューサイズは、内部的にSynchronousQueueインスタンスが使用されるため、常にゼロになります。 SynchronousQueueでは、insert操作とremove操作のペアが常に同時に発生するため、キューに実際に何も含まれることはありません。

Executors.newSingleThreadExecutor() APIは、単一のスレッドを含むThreadPoolExecutorの別の典型的な形式を作成します。 The single thread executor is ideal for creating an event loop.corePoolSizeおよびmaximumPoolSizeパラメーターは1に等しく、keepAliveTimeはゼロです。

上記の例のタスクは順番に実行されるため、タスクの完了後、フラグ値は2になります。

AtomicInteger counter = new AtomicInteger();

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
    counter.set(1);
});
executor.submit(() -> {
    counter.compareAndSet(1, 2);
});

さらに、このThreadPoolExecutorは不変のラッパーで装飾されているため、作成後に再構成することはできません。 これが、ThreadPoolExecutorにキャストできない理由でもあることに注意してください。

3.3. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutorは、ThreadPoolExecutorクラスを拡張し、いくつかの追加メソッドを使用してScheduledExecutorServiceインターフェイスを実装します。

  • scheduleメソッドを使用すると、指定した遅延の後にタスクを1回実行できます。

  • scheduleAtFixedRateメソッドを使用すると、指定された初期遅延の後にタスクを実行し、特定の期間で繰り返し実行できます。 period引数は時間measured between the starting times of the tasksであるため、実行率は固定されています。

  • scheduleWithFixedDelayメソッドは、指定されたタスクを繰り返し実行するという点でscheduleAtFixedRateに似ていますが、指定された遅延はmeasured between the end of the previous task and the start of the nextです。実行率は、特定のタスクの実行にかかる時間によって異なる場合があります。

Executors.newScheduledThreadPool()メソッドは通常、指定されたcorePoolSize、無制限のmaximumPoolSize、およびゼロのkeepAliveTimeScheduledThreadPoolExecutorを作成するために使用されます。 タスクを500ミリ秒で実行するようにスケジュールする方法は次のとおりです。

ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
executor.schedule(() -> {
    System.out.println("Hello World");
}, 500, TimeUnit.MILLISECONDS);

次のコードは、500ミリ秒の遅延後にタスクを実行し、100ミリ秒ごとにタスクを繰り返す方法を示しています。 タスクをスケジュールした後、CountDownLatchロック,を使用して3回起動するまで待機し、Future.cancel()メソッドを使用してタスクをキャンセルします。

CountDownLatch lock = new CountDownLatch(3);

ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
ScheduledFuture future = executor.scheduleAtFixedRate(() -> {
    System.out.println("Hello World");
    lock.countDown();
}, 500, 100, TimeUnit.MILLISECONDS);

lock.await(1000, TimeUnit.MILLISECONDS);
future.cancel(true);

3.4. ForkJoinPool

ForkJoinPoolは、Java 7で導入されたfork/joinフレームワークの中心的な部分です。 spawning multiple tasks in recursive algorithmsの一般的な問題を解決します。 すべてのタスクまたはサブタスクを実行するには独自のスレッドが必要なため、単純なThreadPoolExecutorを使用すると、スレッドがすぐに不足します。

fork/joinフレームワークでは、任意のタスクが多数のサブタスクを生成(fork)し、joinメソッドを使用してそれらの完了を待つことができます。 fork/joinフレームワークの利点は、does not create a new thread for each task or subtaskであり、代わりにWorkStealingアルゴリズムを実装することです。 このフレームワークについては、記事「Guide to the Fork/Join Framework in Java」で詳しく説明されています。

ForkJoinPoolを使用してノードのツリーをトラバースし、すべての葉の値の合計を計算する簡単な例を見てみましょう。 これは、ノード、int値、および子ノードのセットで構成されるツリーの簡単な実装です。

static class TreeNode {

    int value;

    Set children;

    TreeNode(int value, TreeNode... children) {
        this.value = value;
        this.children = Sets.newHashSet(children);
    }
}

ここで、ツリー内のすべての値を並列に合計する場合は、RecursiveTask<Integer>インターフェイスを実装する必要があります。 各タスクは独自のノードを受け取り、その値をそのchildrenの値の合計に追加します。 children値の合計を計算するために、タスクの実装は次のことを行います。

  • childrenセットをストリーミングします。

  • このストリームにマップし、要素ごとに新しいCountingTaskを作成します。

  • 各サブタスクをフォークして実行します。

  • フォークされた各タスクでjoinメソッドを呼び出すことにより、結果を収集します。

  • Collectors.summingIntコレクターを使用して結果を合計します。

public static class CountingTask extends RecursiveTask {

    private final TreeNode node;

    public CountingTask(TreeNode node) {
        this.node = node;
    }

    @Override
    protected Integer compute() {
        return node.value + node.children.stream()
          .map(childNode -> new CountingTask(childNode).fork())
          .collect(Collectors.summingInt(ForkJoinTask::join));
    }
}

実際のツリーで計算を実行するコードは非常に簡単です。

TreeNode tree = new TreeNode(5,
  new TreeNode(3), new TreeNode(2,
    new TreeNode(2), new TreeNode(8)));

ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
int sum = forkJoinPool.invoke(new CountingTask(tree));

4. グアバでのスレッドプールの実装

Guavaは、人気のあるGoogleのユーティリティライブラリです。 ExecutorServiceのいくつかの便利な実装を含む、多くの便利な並行性クラスがあります。 実装クラスは、直接インスタンス化またはサブクラス化するためにアクセスできないため、インスタンスを作成するための唯一のエントリポイントは、MoreExecutorsヘルパークラスです。

4.1. Mavenの依存関係としてGuavaを追加する

Maven pomファイルに次の依存関係を追加して、Guavaライブラリをプロジェクトに含めます。 Guavaライブラリの最新バージョンはMaven Centralリポジトリにあります。


    com.google.guava
    guava
    19.0

4.2. ダイレクトエグゼキュータおよびダイレクトエグゼキュータサービス

状況によっては、現在のスレッドまたはスレッドプールでタスクを実行したい場合があります。 単一のExecutorインターフェースを使用して、実装を切り替えることをお勧めします。 現在のスレッドでタスクを実行するExecutorまたはExecutorServiceの実装を思い付くのはそれほど難しくありませんが、それでもいくつかの定型コードを書く必要があります。

嬉しいことに、グアバは事前定義済みのインスタンスを提供してくれます。

同じスレッドでのタスクの実行を示すHere’s an example。 提供されたタスクは500ミリ秒スリープしますが、blocks the current threadであり、結果はexecute呼び出しが終了した直後に利用できます。

Executor executor = MoreExecutors.directExecutor();

AtomicBoolean executed = new AtomicBoolean();

executor.execute(() -> {
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    executed.set(true);
});

assertTrue(executed.get());

directExecutor()メソッドによって返されるインスタンスは、実際には静的シングルトンであるため、このメソッドを使用しても、オブジェクトの作成にオーバーヘッドはまったく発生しません。

このAPIは、呼び出しごとに本格的なエグゼキュータサービスの実装を作成するため、MoreExecutors.newDirectExecutorService()よりもこのメソッドを使用することをお勧めします。

4.3. エグゼキュータサービスの終了

もう1つの一般的な問題は、スレッドプールがまだタスクを実行している間のshutting down the virtual machineです。 キャンセルメカニズムが設定されていても、executorサービスがシャットダウンしたときにタスクが適切に動作し、作業を停止する保証はありません。 これにより、タスクが作業を続けている間、JVMが無期限にハングする可能性があります。

この問題を解決するために、グアバでは既存のエグゼキューターサービスのファミリーを導入しています。 それらはdaemon threads which terminate together with the JVMに基づいています。

これらのサービスは、Runtime.getRuntime().addShutdownHook()メソッドを使用してシャットダウンフックを追加し、ハングしたタスクを放棄する前に、VMが構成された時間終了するのを防ぎます。

次の例では、無限ループを含むタスクを送信していますが、VMの終了時にタスクを待機するために、100ミリ秒の構成時間で終了するエグゼキューターサービスを使用しています。 exitingExecutorServiceが配置されていないと、このタスクによってVMが無期限にハングします。

ThreadPoolExecutor executor =
  (ThreadPoolExecutor) Executors.newFixedThreadPool(5);
ExecutorService executorService =
  MoreExecutors.getExitingExecutorService(executor,
    100, TimeUnit.MILLISECONDS);

executorService.submit(() -> {
    while (true) {
    }
});

4.4. リスニングデコレータ

リスニングデコレータを使用すると、単純なFutureインスタンスの代わりに、ExecutorServiceをラップして、タスクの送信時にListenableFutureインスタンスを受け取ることができます。 ListenableFutureインターフェースはFutureを拡張し、単一の追加メソッドaddListenerを持っています。 このメソッドにより、将来の完了時に呼び出されるリスナーを追加できます。

ListenableFuture.addListener()メソッドを直接使用することはめったにありませんが、それはessential to most of the helper methods in the Futures utility classです。 たとえば、Futures.allAsList()メソッドを使用すると、複数のListenableFutureインスタンスを単一のListenableFutureに結合できます。これは、結合されたすべての先物が正常に完了すると完了します。

ExecutorService executorService = Executors.newCachedThreadPool();
ListeningExecutorService listeningExecutorService =
  MoreExecutors.listeningDecorator(executorService);

ListenableFuture future1 =
  listeningExecutorService.submit(() -> "Hello");
ListenableFuture future2 =
  listeningExecutorService.submit(() -> "World");

String greeting = Futures.allAsList(future1, future2).get()
  .stream()
  .collect(Collectors.joining(" "));
assertEquals("Hello World", greeting);

5. 結論

この記事では、標準のJavaライブラリとGoogleのGuavaライブラリでのスレッドプールパターンとその実装について説明しました。

記事のソースコードはover on GitHubで入手できます。