[Java] スレッドの使い方

わかりやすいJava_オブジェクト指向編を読んで勉強中、スレッドの解説は非常にわかり易かった。

スレッドで同時並行処理の最も簡単な例

public class SimpleThread {

  public static void main(String[] args) {
    for(int i = 0; i < 50; i++) {

      // Runnableインタフェースを実装したインスタンスを
      // コンストラクタに渡してThreadインスタンスを生成
      Thread thread = new Thread(createRunnable(i));

      // Thread#start で Runnable#run を生成したスレッドで実行する
      // ※Thread#startによりスレッドが起動して、実行待ち状態になる
      thread.start();
    }
  }

  private static Runnable createRunnable(final int number) {
    return new Runnable() {
      @Override
      public void run() {
        System.out.println(number);
      }
    };
  }
}

結果は 0 ~ 49 までの数値がバラバラ(ランダムではなく、ある程度順番通りではあるけど…)に表示される。 わざわざRunnableを実装したクラスを作成するのは面倒なので、無名クラスを使うと楽。

Thread 自体が Runnable を実装している(実装に具体的な中身は無い)ので Thread サブクラスを無名クラスで作るほうが楽(だけど継承の方法としては推奨されない)

Thread

public class ThreadHasName {

  public static void main(String[] args) {
    // スレッドには名前がある
    Thread currentThread = Thread.currentThread();
    System.out.println(currentThread.getName());

    // 名前を変更(セット)
    currentThread.setName("メインスレッド");
    System.out.println(currentThread.getName());

    // 名前をコンストラクタで命名
    // Threadの無名クラスを利用
    new Thread("スレッド名") {
      @Override
      public void run() {
        System.out.println(Thread.currentThread().getName());
      }
    }.start();
  }
}

結果

main
メインスレッド
スレッド名

※結果の出力される順番は前後する可能性あり

スレッドの状態遷移

状態遷移の図はテキトーにググると出てくる -> java thread state - Google 検索

  • 初期状態から始まり、終了状態で終わる。初期状態はThread#start()する前の状態で、終了状態はThread#start()で開始した処理が完了した状態。
  • 初期状態 -> (実行可能プール・実行中・実行不可能) -> 終了状態
  • 終了状態となっても Thread インスタンスは消えないが、スレッドを再び実行することはできない。
  • 実行不能状態から実行状態に直接状態遷移することはない。 Thread#start() により 初期状態 -> 実行可能プール へと状態遷移する
  • 実行可能プールからJVMのスケジューラが実行するスレッドを選んで実行すると、そのスレッドは実行中となる。
  • 実行中のスレッドはスケジューラにより実行可能プールに戻される
  • 実行中のスレッドは wait, sleep, block により実行不可能状態に状態遷移する。
  • wait により実行不可(待機)状態となったものは、他スレッドの notify, notifyAll によって実行可能プールに復帰する

Thread#join

スレッドを割り込ませて、同期的に処理する。例えば 実行中の Thread-AThread-Bjoin すると、 Thread-B が終了状態になるまで Thread-A は実行不可能となる。割り込んだスレッドが現在のスレッドのロックを取得して終了時にロックを離す…ってことかな。

public class ThreadJoin {

  private static int number = 0;

  public static void main(String[] args) throws Exception {

    Runnable runnable = new Runnable() {
      @Override
      public synchronized void run() {
        number++;
      }
    };

    for (int i = 0; i < 10; i++) {
      Thread thread = new Thread(runnable);
      thread.start();
      thread.join();
      System.out.println(number);
    }

    System.out.println("ここは最後に出力される!");
  }

}

結果

1
2
3
4
5
6
7
8
9
10
ここは最後に出力される!

join の引数に時間を指定した場合

public class JoinSample {

  public static void main(String[] args) {

    Thread thread = new Thread() {
      @Override
      public void run() {
        try {
          Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println("ここは別スレッドの処理");
      }
    };

    thread.start();

    try {
      thread.join(1000 * 3);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

    System.out.println("メインスレッド終了");
  }
}

指定した時間のうちは同期的に実行される

結果

メインスレッド終了
ここは別スレッドの処理

Thread#interrupt

Thread#interrupt により、そのスレッドの休止状態やJOINの処理を中断させる。

public class ThreadSleep {

  public static void main(String[] args) throws Exception {

    Thread thread = new Thread() {
      @Override
      public void run() {
        System.out.println("START");

        try {
          Thread.sleep(1000 * 60); // 1分
        } catch (InterruptedException e) {
          System.out.println("割り込みにより sleep 終了");
        }

        System.out.println("END");
      }
    };
    thread.start();

    Thread.sleep(1000 * 3); // 3秒
    thread.interrupt();

  }
}

約3秒でこの処理は終了する。

結果

START
割り込みにより sleep 終了
END

Thread#priority フィールド

スレッドに優先順位の設定をすることが可能で、デフォルトは Thread.NORM_PRIORITY となっている。 最小は Thread.MIN_PRIORITY(1), 最大は Thread.MAX_PRIORITY(10) だが JVM によって範囲は異なる可能性がある。 Thread#priority フィールドにアクセサを通じて優先順位の設定・取得が可能。 優先順位が高いからといって、優先順位が低いものより先に実行されるとは限らない。実行順は JVM のスケジューラが決める。

Thread.yield メソッド

実行中のスレッドを、実行可能プールに戻すだけ。 実行可能プールに戻してもすぐに実行中に状態遷移することがある。

同期化

public class Sync {

  public synchronized void method1() {
    // medhot2と等価
  }

  public synchronized void method2(){
    synchronized (this) {
      // 自分自身のインスタンスのロックを使用
    }
    // synchronizedブロックを抜けるとロックを解放
  }

  public synchronized void staticMethod1(){
    // staticMethod2と等価
  }

  public void staticMethod2(){
    synchronized (Sample.class) {
      // メタクラスインスタンスのロックを取得
    }
  }  
}

メソッドに synchronized を付けるのは糖衣構文

waitnotify

  • Thraed#waitThread#notify, Thread#notifyAll により、1つのロックに対して複数のスレッドを協調して動作させることができる。
  • Thread#wait により待機状態となったスレッドは、他スレッドの Thread#notify によって実行可能プールへと戻されて復帰する。
  • Thread#notify によってロック待ちのスレッドにロックが解放されたことを知らせる。
  • wait, notifysynchronized ブロック内で使用しないと意味が無い(非同期だと勝手にロック解放してしまうので協調不能)

waitnotify で知らせを受けるまで待機し続ける

public class WaitForever {

  public static void main(String[] args) throws Exception {
    Thread.currentThread().wait();
    System.out.println("永遠に実行されない");
  }

}

同じロックに対して実行すること

public class Sample {

  public static void main(String[] args) throws Exception {

    System.out.println("START");

    final Sample sample = new Sample();

    new Thread() {
      @Override
      public void run() {
        try {
          sample.method1();
        } catch (Exception e) {
        }
      };
    }.start();

    Thread.sleep(1000 * 3);

    new Thread() {
      @Override
      public void run() {
        try {
          sample.method2();
        } catch (Exception e) {
        }
      }
    }.start();

  }

  private synchronized void method1() throws Exception {
    this.wait();
    System.out.println("recovery");
  }

  private synchronized void method2() throws Exception {
    this.notify();
    System.out.println("notify");
  }

}

結果

START (約3秒待ち)
notify
recovery

生産者-消費者問題

public class Sample {

  public static void main(String[] args) {
    final Creator creator = new Creator();

    for (int i = 0; i < 10; i++) {
      new Thread() {
        @Override
        public void run() {
          try {
            creator.createSomething();
          } catch (Exception e) {
          }
        };
      }.start();

      new Thread() {
        @Override
        public void run() {
          try {
            creator.consumeSomething();
          } catch (Exception e) {
            e.printStackTrace();
          }
        };
      }.start();

    } // end of for
  }
}

class Creator {

  private int number = 0;

  private String something = null;

  public synchronized void createSomething() throws Exception {
    while (something != null) {
      wait();
    }

    Thread.sleep(1000);
    something = "素敵な何か(" + number++ + "番目)";
    notifyAll();
  }

  public synchronized void consumeSomething() throws Exception {
    while (something == null) {
      wait();
    }

    doSomething(something);
    something = null;
    notifyAll();
  }

  public void doSomething(final String something) {
    new Thread() {
      @Override
      public void run() {
        // 何らかの処理
        System.out.println(something);
      }
    }.start();
  }
}

結果

素敵な何か(0番目)
素敵な何か(1番目)
素敵な何か(2番目)
素敵な何か(3番目)
素敵な何か(4番目)
素敵な何か(5番目)
素敵な何か(6番目)
素敵な何か(7番目)
素敵な何か(8番目)
素敵な何か(9番目)
Share
関連記事