1. 序章

このチュートリアルでは、スレッドを開始して並列タスクを実行するさまざまな方法を検討します。

これは、特にメインスレッドで実行できない長い操作や繰り返しの操作を処理する場合、または操作の結果を待っている間UIインタラクションを保留にできない場合に非常に便利です。

スレッドの詳細について詳しくは、Javaでのスレッドのライフサイクルに関するチュートリアルを必ずお読みください。

2. スレッドの実行の基本

Thread フレームワークを使用すると、並列スレッドで実行されるロジックを簡単に記述できます。

Thread クラスを拡張して、基本的な例を試してみましょう。

public class NewThread extends Thread {
    public void run() {
        long startTime = System.currentTimeMillis();
        int i = 0;
        while (true) {
            System.out.println(this.getName() + ": New Thread is running..." + i++);
            try {
                //Wait for one sec so it doesn't print too fast
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ...
        }
    }
}

次に、スレッドを初期化して開始するための2番目のクラスを作成します。

public class SingleThreadExample {
    public static void main(String[] args) {
        NewThread t = new NewThread();
        t.start();
    }
}

NEW 状態(開始されていないのと同等)のスレッドで start()メソッドを呼び出す必要があります。 それ以外の場合、JavaはIllegalThreadStateException例外のインスタンスをスローします。

ここで、複数のスレッドを開始する必要があると仮定しましょう。

public class MultipleThreadsExample {
    public static void main(String[] args) {
        NewThread t1 = new NewThread();
        t1.setName("MyThread-1");
        NewThread t2 = new NewThread();
        t2.setName("MyThread-2");
        t1.start();
        t2.start();
    }
}

私たちのコードはまだ非常に単純で、オンラインで見つけることができる例と非常によく似ています。

もちろん、これは本番環境に対応したコードとはほど遠いものです。コンテキストの切り替えやメモリの使用量が多すぎないように、リソースを正しい方法で管理することが非常に重要です。

したがって、本番環境に対応するには、次の処理を行うために追加の定型文を作成する必要があります。

  • 新しいスレッドの一貫した作成
  • 同時ライブスレッドの数
  • スレッドの割り当て解除:リークを回避するためにデーモンスレッドにとって非常に重要

必要に応じて、これらすべてのケースシナリオやさらにいくつかのシナリオに対して独自のコードを記述できますが、なぜ車輪の再発明を行う必要があるのでしょうか。

3. ExecutorServiceフレームワーク

ExecutorService は、スレッドプールデザインパターン(レプリケートされたワーカーまたはワーカークルーモデルとも呼ばれます)を実装し、前述のスレッド管理を処理します。さらに、スレッドの再利用性やタスクキューなどの非常に便利な機能を追加します。 。

特に、スレッドの再利用性は非常に重要です。大規模なアプリケーションでは、多くのスレッドオブジェクトの割り当てと割り当て解除により、メモリ管理のオーバーヘッドが大幅に増加します。

ワーカースレッドを使用すると、スレッドの作成によって発生するオーバーヘッドを最小限に抑えることができます。

プールの構成を容易にするために、 ExecutorService には、簡単なコンストラクターと、キューのタイプ、スレッドの最小数と最大数、およびそれらの命名規則などのいくつかのカスタマイズオプションが付属しています。

ExecutorService、の詳細については、 JavaExecutorServiceガイドをお読みください。

4. エグゼキュータでタスクを開始する

この強力なフレームワークのおかげで、スレッドの開始からタスクの送信に考え方を切り替えることができます。

非同期タスクをエグゼキュータに送信する方法を見てみましょう。

ExecutorService executor = Executors.newFixedThreadPool(10);
...
executor.submit(() -> {
    new Task();
});

使用できるメソッドは2つあります。何も返さないexecuteと、計算結果をカプセル化するFutureを返すsubmitです。

Futures、の詳細については、java.util.concurrent.Futureガイドをお読みください。

5. CompleteableFuturesでタスクを開始する

Future オブジェクトから最終結果を取得するには、オブジェクトで使用可能な get メソッドを使用できますが、これにより、計算が終了するまで親スレッドがブロックされます。

または、タスクにロジックを追加することでブロックを回避することもできますが、コードの複雑さを増す必要があります。

Java 1.8では、 Future 構造の上に新しいフレームワークが導入され、計算結果をより適切に処理できるようになりました。CompleteableFutureです。

CompletableFutureCompletableStageを実装します。これにより、コールバックをアタッチし、準備ができた後に結果に対して操作を実行するために必要なすべての配管を回避するためのメソッドの幅広い選択肢が追加されます。

タスクを送信するための実装ははるかに簡単です。

CompletableFuture.supplyAsync(() -> "Hello");

supplyAsync は、非同期で実行するコード(この場合はラムダパラメーター)を含むSupplierを取ります。

タスクは暗黙的にForkJoinPool.commonPool()に送信されるか、2番目のパラメーターとしてExecutorを指定できます。

CompletableFuture、の詳細については、CompletableFutureガイドをお読みください。

6. 遅延または定期的なタスクの実行

複雑なWebアプリケーションを操作する場合、特定の時間に、場合によっては定期的にタスクを実行する必要があります。

Javaには、遅延または繰り返しの操作を実行するのに役立つツールがいくつかあります。

  • java.util.Timer
  • java.util.concurrent.ScheduledThreadPoolExecutor

6.1。タイマー

Timer は、バックグラウンドスレッドで将来実行されるタスクをスケジュールする機能です。

タスクは、1回だけ実行するようにスケジュールすることも、定期的に繰り返し実行するようにスケジュールすることもできます。

1秒の遅延後にタスクを実行する場合、コードがどのように見えるかを見てみましょう。

TimerTask task = new TimerTask() {
    public void run() {
        System.out.println("Task performed on: " + new Date() + "n" 
          + "Thread's name: " + Thread.currentThread().getName());
    }
};
Timer timer = new Timer("Timer");
long delay = 1000L;
timer.schedule(task, delay);

次に、定期的なスケジュールを追加しましょう。

timer.scheduleAtFixedRate(repeatedTask, delay, period);

今回は、指定された遅延の後にタスクが実行され、一定期間が経過するとタスクが繰り返されます。

詳細については、 JavaTimerのガイドをお読みください。

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor には、Timerクラスと同様のメソッドがあります。

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
ScheduledFuture<Object> resultFuture
  = executorService.schedule(callableTask, 1, TimeUnit.SECONDS);

この例を終了するために、定期的なタスクに scheduleAtFixedRate()を使用します。

ScheduledFuture<Object> resultFuture
 = executorService.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);

上記のコードは、100ミリ秒の初期遅延の後にタスクを実行し、その後、450ミリ秒ごとに同じタスクを実行します。

プロセッサが次の発生までに時間内にタスクの処理を完了できない場合、 ScheduledExecutorService は現在のタスクが完了するまで待機してから、次のタスクを開始します。

この待機時間を回避するために、 scheduleWithFixedDelay()を使用できます。これは、その名前で説明されているように、タスクの反復間の固定長の遅延を保証します。

ScheduledExecutorService、の詳細については、 JavaExecutorServiceガイドをお読みください。

6.3. どのツールが優れていますか?

上記の例を実行すると、計算結果は同じように見えます。

では、どのようにして適切なツールを選択するのでしょうか。

フレームワークが複数の選択肢を提供する場合、情報に基づいた意思決定を行うために、基盤となるテクノロジーを理解することが重要です。

ボンネットの下でもう少し深く潜ってみましょう。

タイマー

  • リアルタイムの保証は提供しません。Object.wait(long)メソッドを使用してタスクをスケジュールします。
  • バックグラウンドスレッドが1つしかないため、タスクは順番に実行され、実行時間の長いタスクは他のスレッドを遅らせる可能性があります
  • TimerTask でスローされたランタイム例外は、使用可能な唯一のスレッドを強制終了するため、Timerを強制終了します。

ScheduledThreadPoolExecutor

  • 任意の数のスレッドで構成できます
  • 利用可能なすべてのCPUコアを利用できます
  • 実行時の例外をキャッチし、必要に応じて処理できるようにします(ThreadPoolExecutorからafterExecuteメソッドをオーバーライドすることにより)
  • 他の人に実行を継続させながら、例外をスローしたタスクをキャンセルします
  • タイムゾーン、遅延、太陽時などを追跡するためにOSスケジューリングシステムに依存しています。
  • 送信されたすべてのタスクの完了を待つなど、複数のタスク間の調整が必要な場合に、コラボレーションAPIを提供します
  • スレッドのライフサイクルを管理するためのより優れたAPIを提供します

今の選択は明らかですよね?

7. FutureScheduledFutureの違い

コード例では、ScheduledThreadPoolExecutorが特定のタイプのFutureScheduledFutureを返すことがわかります。

ScheduledFuture は、FutureDelayedの両方のインターフェースを拡張し、現在のタスクに関連付けられた残りの遅延を返す追加のメソッドgetDelayを継承します。 RunnableScheduledFuture によって拡張され、タスクが定期的かどうかをチェックするメソッドが追加されます。

ScheduledThreadPoolExecutor は、内部クラス ScheduledFutureTask を介してこれらすべての構成を実装し、それらを使用してタスクのライフサイクルを制御します。

8. 結論

このチュートリアルでは、スレッドを開始してタスクを並行して実行するために使用できるさまざまなフレームワークを実験しました。

次に、TimerScheduledThreadPoolExecutor。の違いについて詳しく説明しました。

この記事のソースコードは、GitHubから入手できます。