Javaで非同期処理や並列処理を実行するにはスレッドを扱う必要があります。
この記事は、Java初〜中級者がスレッドについてざっくり理解できるようになるといいなあという感じで書いております。
スレッドとはなんぞや
スレッドとは、プログラムの実行時にたどる道筋のようなもの。
たとえば
public static void main(String[] args) {
run();
}
static void run() {
int result = sum(1, 2);
System.out.println(result);
}
static int sum(int a, int b) {
return a + b;
}
というプログラムを実行すると、
main()が呼ばれて、run()が呼ばれて、sum()が呼ばれて、System.out.println()が呼ばれて、、、のように処理を一つずつ実行しながらたどっていく。
この一筋の道をたどりながら処理を実行していくやつがスレッドです。
すべての処理はスレッドの上で動く
例えばmain関数から処理を実行すると...
public static void main(String[] args) {
Thread currentThread = Thread.currentThread(); // 自分自身のスレッドを取得
System.out.printf("ID:%d, Name:%s, Priority:%d, State:%s%n",
currentThread.getId(),
currentThread.getName(),
currentThread.getPriority(),
currentThread.getState());
}
実行結果
ID:1, Name:main, Priority:5, State:RUNNABLE
main
という名前のスレッドで実行されたのがわかる
別スレッドでなにか処理を実行するコードを書いてみる
mainスレッドから別スレッドを分岐して非同期に何か処理を実行してみる。
Thread
クラスをnewし、start()
を呼ぶ。
public class Sample {
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> { // Threadを作成。コンストラクタに実行したい処理を渡す
for (int i = 1; i < 5; i++) {
System.out.println("thread: " + i);
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
return;
}
}
});
System.out.println("main: 1");
thread.start(); // スレッドの処理を開始
System.out.println("main: 2");
Thread.sleep(150L);
System.out.println("main: 3");
thread.join(); // スレッドの終了を待機(待たない場合はやらないでも特に問題ない)
System.out.println("main: 4");
}
}
出力結果
main: 1
main: 2
thread: 1
thread: 2
main: 3
thread: 3
thread: 4
main: 4
mainスレッドの処理と生成したスレッドの処理はそれぞれ独立して動いているのがわかる
スレッドはどんな情報を持っているか
JavaではThread
クラスを通してスレッドを扱う。
IntellijでThreadクラスのプロパティを見るとこんな感じ
よく見かけるものだけ説明します。
ID :long
スレッド生成時に自動的に割り振られるlong型の値。一意で不変。
name :String
スレッドの名前。
setName(String)
で任意の名前を設定可能。
new Thread()
とかでスレッドを作るとThread-1
、Thread-2
みたいな名前が勝手に設定される
state :Thread.State
スレッドの状態。「実行中」とか「待機中」とか。
スレッドは状態を持っていて、NEW
,RUNNABLE
,BLOCKED
,WAITING
,TIMED_WAITING
,TERMINATED
のいずれかの状態にある。
詳細はJavadoc参照
interrupted :boolean
スレッドが中断されたかどうか。
-
Thread#interrupt()
でこの値をtrueにすることができる -
Thread#isInterrupted()
もしくはThread.interrupted()
で中断されたかどうかを取得できる(違いは後ほど説明)
※ Thread#interrupt()
を呼んでも実行中のスレッドが直ちに停止するわけではない。このメソッドはinterrupted
というフィールドをtrueに設定するだけ。(これも後ほど説明)
priority :int
スレッドの優先度。
資源が競合した際、高い優先度を持つスレッドの方が優先的に実行される。
デフォルトは5(Thread.NORM_PRIORITY
)で、最低が1(Thread.MIN_PRIORITY
)で、最高が10(Thread.MAX_PRIORITY
)。
setPriority(int)
で変更可能。
daemon: boolean
デーモンスレッドかどうか。
daemon
がtrue
のものをデーモンスレッド、false
のものはユーザースレッドと呼ぶ。
デフォルトfalse
で、setDaemon(boolean)
で設定可能。
※Javaのプロセスは全てのユーザースレッドが終了すると自動的に終了する
つまり、デーモンスレッドが何か実行中でも、完了を待たずにプロセスは終了する。
いろいろなスレッドプログラミング
別スレッドで何か実行するプログラムを書く方法はいろいろある。
生のThreadを使う方法
Thread
のコンストラクタ引数に実行する処理をラムダとかで入れてnewし、start()
を呼ぶことでスレッドが開始される。
Thread thread = new Thread(() -> {
// なにかの処理...
});
thread.start(); // スレッドを開始。「なにかの処理」が非同期に実行される
Thread
を継承しrun
をオーバーライドしてもよい。
(が、個人的には↑の方法のほうがおすすめです。処理がThread
クラスに依存してしまうので。)
class MyThread extends Thread {
@Override
public void run() {
// なにかの処理...
}
}
MyThread thread = new MyThread();
thread.start(); // スレッドを開始。「なにかの処理」が非同期に実行される
Executorを使う方法
簡単にスレッドプールみたいなものが作成できる。
ExecutorService threadPool = Executors.newFixedThreadPool(3); // スレッド数=3のスレッドプールを作成
// 100個処理を投入してみる
for (int i = 0; i < 100; i++) {
threadPool.submit(() -> { // threadPoolは3個のスレッドを駆使して投入された処理をどんどん実行していく
// なにかの処理...
});
}
// submitされたすべての処理が終了するのを待機
threadPool.awaitTermination(1, TimeUnit.MINUTES);
// スレッドプールを停止
threadPool.shutdown();
Executors.newFixedThreadPool()
以外にもいろいろある。
newCachedThreadPool()
newFixedThreadPool()
newScheduledThreadPool()
newSingleThreadExecutor()
newWorkStealingPool()
CompletableFutureを使う方法
JavaScriptで言うところのFuture/Promise的なもの。
内部的にはスレッド制御にExecutorが使用されている
// 処理を非同期に実行する
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
// なにかの処理...
});
// 戻り値のCompletableFutureにコールバックを登録できる
future.whenComplete((aVoid, throwable) -> { // JavaScriptのpromiseで言うところの.then()
// 「なにかの処理」が終わったらここが呼ばれる
// - 「なにかの処理」が正常に完了した場合、引数のthrowableはnull
// - 「なにかの処理」で例外が発生した場合、引数のthrowableにその例外が渡される
});
スレッドの中断
中断命令を出す
実行中のスレッドの処理を中断させたいときは、Thread#interrupt()
を使う
Thread thread = createNanikanoThread();
thread.start(); // スレッドを動かす
...
thread.interrupt(); // 動いているスレッドに対して中断命令を送る
だがしかし、interrupt()
したからといって、対象のスレッドがすぐに止まるとは限らないッ!
このメソッドを呼ぶと何が行われるかというと、
対象スレッドが持つinterrupted
という名前のbooleanフィールドにtrue
がセットされるだけのイメージ(←多少乱暴な説明)。
基本的には中断される側の処理でこのinterrupted
フラグを確認しなければならない。
中断状態の確認
中断(interrupt)される可能性のある処理は、それを想定した実装をしなければならない。
中断されたかどうか(interrupted
フラグがONになっているかどうか)はThread.interrupted()
で確認できる。
// ずっと何かをする処理
while (true) {
if (Thread.interrupted()) { // 中断されたかどうか確認する(※このメソッドを呼ぶとinterruptedフラグはfalseに戻る)
throw new InterruptedException(); // 例外を投げて処理を抜ける。基本的にはInterruptedExceptionを使えばOK
}
// 何かの処理
...
}
Thread.interrupted()
を呼んで状態を確認すると、interruptedフラグはfalseにリセットされる。
(ちなみにThread.currentThread().isInterrupted()
で確認すればリセットされない)
メソッドによってはinterrupted
状態の確認をやってくれるようなものもある。
たとえばThread.sleep()
は、スリープ中にinterrupted
フラグが立ったらスリープをやめてInterruptedException
をスローしてくれる。
(ちなみにこのときにもinterruptedフラグはfalseにリセットされる)
// ずっと何かをする処理
while (true) {
Thread.sleep(1000L); // 処理を1秒間休止。休止中にinterruptされたらInterruptedExceptionがスローされる
...
}
InterruptedExceptionの扱い
InterruptedExceptionがスローされたということは、スレッドが中断されました〜という情報を受け取ったということ。
この情報を失わせずに適切な処理をしなければならない。
だめな例1: 中断命令を無視して処理を続行してしまう
スレッドが中断されましたよ〜という情報を無視して続行しちゃっている
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
}
だめな例2: 別の例外でラップしてスローする
よく見かけるやつ
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
スレッドが中断されましたよ〜という情報が失われてしまっている。
外側の処理でこの例外をcatchしたときに、予期しないエラーとして扱われてしまう。
悪くない例: interruptedフラグを再びONにしてから別の例外でラップしてスローする
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt(); // Here!
throw new RuntimeException(ex);
}
interruptedフラグの判定を外側の処理に任せる。
中断されたよ〜という情報が生き残る。
もちろん外側でこれをキャッチする処理は、interruptedされたかどうか判断して何かしなきゃいけない
個人的に編み出したプラクティス: 専用の非検査例外を作る
(この方法がいいのかどうかはわからない。もっといい方法あったら教えてください...)
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
throw new InterruptedRuntimeException(ex);
}
外側の処理で必ずこのInterruptedRuntimeExceptionをキャッチして適切な処理をする。
sleepするときにいちいちtry-catchするのも面倒なのでユーティリティクラスを作っています
public class ThreadUtils {
/**
* @param n スリープする時間(ミリ秒)
* @throws InterruptedRuntimeException スレッドが中断されていた場合
*/
public static void sleep(long n) {
try {
Thread.sleep(n);
} catch (InterruptedException ex) {
throw new InterruptedRuntimeException(ex);
}
}
/**
* 現在のスレッドが中断されたかチェックする.
* @throws InterruptedRuntimeException スレッドが中断されていた場合
*/
public static void checkForInterruption() {
if (Thread.interrupted()) {
throw new InterruptedRuntimeException(ex);
}
}
}
スタックトレースについて
WIP