25
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

雑に理解するJavaのスレッド

Last updated at Posted at 2018-12-26

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クラスのプロパティを見るとこんな感じ

Screen Shot 2018-12-17 at 11.40.57.png

よく見かけるものだけ説明します。

ID :long

スレッド生成時に自動的に割り振られるlong型の値。一意で不変。

name :String

スレッドの名前。
setName(String)で任意の名前を設定可能。
new Thread()とかでスレッドを作るとThread-1Thread-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

デーモンスレッドかどうか。
daemontrueのものをデーモンスレッド、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

25
26
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?