Javaでスレッドを開始する方法
1. 前書き
このチュートリアルでは、スレッドを開始して並列タスクを実行するさまざまな方法を検討します。
This is very useful, in particular when dealing with long or recurring operations that can’t run on the main thread、または操作の結果を待っている間、UIインタラクションを保留にできない場合。
スレッドの詳細について詳しくは、Life Cycle of a Thread in 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();
}
}
ここで、複数のスレッドを開始する必要があると仮定しましょう。
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();
}
}
私たちのコードはまだ非常にシンプルで、オンラインで見つけることができる例と非常によく似ています。
もちろん、this is far from production-ready code, where it’s of critical importance to manage resources in the correct way, to avoid too much context switching or too much memory usage.
処理するSo, to get production-ready we now need to write additional boilerplate:
-
新しいスレッドの一貫した作成
-
同時実行スレッドの数
-
スレッドの割り当て解除:リークを回避するために、デーモンスレッドにとって非常に重要
必要に応じて、これらのすべてのケースシナリオ、さらにはさらにいくつかの独自のコードを作成できますが、なぜ車輪を再発明する必要があるのでしょうか。
3. ExecutorServiceフレームワーク
ExecutorServiceは、スレッドプールデザインパターン(レプリケートされたワーカーまたはワーカークルーモデルとも呼ばれます)を実装し、前述のスレッド管理を処理します。さらに、スレッドの再利用性やタスクキューなどの非常に便利な機能を追加します。
特に、スレッドの再利用性は非常に重要です。大規模なアプリケーションでは、多数のスレッドオブジェクトの割り当てと割り当て解除により、メモリ管理のオーバーヘッドが大幅に増加します。
ワーカースレッドを使用すると、スレッドの作成によって発生するオーバーヘッドを最小限に抑えることができます。
プールの構成を容易にするために、ExecutorServiceには、簡単なコンストラクターと、キューのタイプ、スレッドの最小数と最大数、およびそれらの命名規則などのいくつかのカスタマイズオプションが付属しています。
ExecutorService,の詳細については、Guide to the Java ExecutorServiceをお読みください。
4. エグゼキューターを使用したタスクの開始
この強力なフレームワークのおかげで、スレッドの開始からタスクの送信に考え方を切り替えることができます。
非同期タスクをエグゼキュータに送信する方法を見てみましょう。
ExecutorService executor = Executors.newFixedThreadPool(10);
...
executor.submit(() -> {
new Task();
});
使用できるメソッドは2つあります。何も返さないexecuteと、計算結果をカプセル化したFutureを返すsubmitです。
Futures,の詳細については、Guide to java.util.concurrent.Futureをお読みください。
5. CompletableFuturesでタスクを開始する
Futureオブジェクトから最終結果を取得するには、オブジェクトで使用可能なgetメソッドを使用できますが、これにより、計算が終了するまで親スレッドがブロックされます。
または、タスクにロジックを追加してブロックを回避することもできますが、コードの複雑さを増す必要があります。
Java 1.8では、Future構造の上に新しいフレームワークが導入され、計算結果をより適切に処理できるようになりました:CompletableFuture。
CompletableFutureはCompletableStageを実装します。これにより、コールバックをアタッチし、準備ができた後に結果に対して操作を実行するために必要なすべての配管を回避するためのメソッドの幅広い選択肢が追加されます。
タスクを送信するための実装は、はるかに簡単です。
CompletableFuture.supplyAsync(() -> "Hello");
supplyAsyncは、非同期で実行するコード(この場合はラムダパラメーター)を含むSupplierを取ります。
タスクは暗黙的にForkJoinPool.commonPool()に送信されるようになりました。または、2番目のパラメータとして優先するExecutorを指定できます。
CompletableFuture,の詳細については、Guide To CompletableFutureをお読みください。
6. 遅延タスクまたは定期タスクの実行
複雑なWebアプリケーションを操作する場合、特定の時間に、場合によっては定期的にタスクを実行する必要があります。
Javaには、遅延操作または繰り返し操作を実行するのに役立つツールがほとんどありません。
-
java.util.Timer
-
java.util.concurrent.ScheduledThreadPoolExecutor
6.1. Timer
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);
今回は、指定された遅延の後にタスクが実行され、一定期間が経過するとタスクが繰り返されます。
詳細については、Java Timerのガイドをご覧ください。
6.2. ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutorには、Timerクラスと同様のメソッドがあります。
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
ScheduledFuture
この例を終了するために、定期的なタスクにscheduleAtFixedRate()を使用します。
ScheduledFuture
上記のコードは、100ミリ秒の初期遅延の後にタスクを実行し、その後、450ミリ秒ごとに同じタスクを実行します。
プロセッサが次の発生までに時間内にタスクの処理を完了できない場合、ScheduledExecutorServiceは現在のタスクが完了するまで待機してから、次のタスクを開始します。
この待機時間を回避するために、scheduleWithFixedDelay()を使用できます。これは、その名前で説明されているように、タスクの反復間の固定長の遅延を保証します。
ScheduledExecutorService,の詳細については、Guide to the Java ExecutorServiceをお読みください。
6.3. どのツールが優れていますか?
上記の例を実行すると、計算結果は同じように見えます。
では、how do we choose the right tool?
フレームワークが複数の選択肢を提供する場合、情報に基づいた意思決定を行うには、基盤となるテクノロジーを理解することが重要です。
ボンネットの下でもう少し深く潜ってみましょう。
Timer:
-
リアルタイムの保証は提供しません。Object.wait(long) メソッドを使用してタスクをスケジュールします。
-
バックグラウンドスレッドが1つあるため、タスクは順番に実行され、実行時間の長いタスクは他のタスクを遅らせる可能性があります
-
TimerTaskでスローされたランタイム例外は、使用可能な唯一のスレッドを強制終了するため、Timerを強制終了します。
ScheduledThreadPoolExecutor:
-
任意の数のスレッドで構成できます
-
使用可能なすべてのCPUコアを活用できます
-
ランタイム例外をキャッチし、必要に応じて処理できるようにします(ThreadPoolExecutorからafterExecuteメソッドをオーバーライドすることにより)
-
例外を投げたタスクをキャンセルし、他の人に実行を継続させる
-
OSスケジューリングシステムに依存して、タイムゾーン、遅延、太陽時などを追跡します。
-
送信されたすべてのタスクの完了を待機するなど、複数のタスク間の調整が必要な場合に、コラボレーションAPIを提供します
-
スレッドのライフサイクルを管理するためのより良いAPIを提供します
今の選択は明らかですよね?
7. FutureとScheduledFutureの違い
コード例では、ScheduledThreadPoolExecutorが特定のタイプのFuture(ScheduledFuture)を返すことがわかります。
ScheduledFuture は、FutureインターフェースとDelayedインターフェースの両方を拡張するため、現在のタスクに関連付けられた残りの遅延を返す追加のメソッドgetDelayを継承します。 タスクが定期的かどうかを確認するメソッドを追加するRunnableScheduledFutureによって拡張されます。
ScheduledThreadPoolExecutorは、内部クラスScheduledFutureTaskを介してこれらすべての構成を実装し、それらを使用してタスクのライフサイクルを制御します。
8. 結論
このチュートリアルでは、スレッドを開始してタスクを並行して実行するために利用可能なさまざまなフレームワークを実験しました。
次に、TimerとScheduledThreadPoolExecutor.の違いについて詳しく説明しました。
記事のソースコードはover on GitHubで入手できます。