Java マルチスレッドプログラミング

Java はマルチスレッドプログラミングを組み込みでサポートしています。スレッドとは、プロセス内の単一の順次制御フローのことです。1つのプロセスは複数のスレッドを並行して持つことができ、各スレッドは異なるタスクを並行して実行します。

マルチスレッドはマルチタスクの特別な形態ですが、マルチスレッドはより少ないリソースオーバーヘッドで済みます。

ここでは、スレッドに関連するもう1つの用語、プロセスを定義します。プロセスには、オペレーティングシステムによって割り当てられたメモリ空間が含まれ、1つ以上のスレッドが含まれます。スレッドは単独で存在することはできず、プロセスの一部である必要があります。プロセスは、すべての非デーモンスレッドが実行を終了するまで実行され続け、その後終了します。

マルチスレッドは、プログラマーが CPU を十分に活用するための高効率なプログラムを作成するという目的を満たします。


スレッドのライフサイクル

スレッドは動的な実行過程であり、生成から終了までの過程もあります。

次の図は、スレッドの完全なライフサイクルを示しています。

  • 新規作成状態:

    使用newキーワードとThreadクラスまたはそのサブクラスでスレッドオブジェクトを作成すると、そのスレッドオブジェクトは新規作成状態になります。この状態は、プログラムがstart()このスレッド。

  • 実行可能状態:

    スレッドオブジェクトが start() メソッドを呼び出すと、そのスレッドは実行可能状態になります。実行可能状態のスレッドは実行可能キューにあり、JVM 内のスレッドスケジューラによるスケジューリングを待機する必要があります。

  • 実行状態:

    実行可能状態のスレッドが CPU リソースを取得すると、実行できますrun()、この時点でスレッドは実行状態になります。

  • ブロック状態:

    スレッドが sleep(スリープ)、suspend(一時停止)などのメソッドを実行し、占有していたリソースを失った後、そのスレッドは実行状態からブロック状態に入ります。スリープ時間が経過した後、またはデバイスリソースを取得した後、実行可能状態に再び入ることができます。次の3つに分類できます:

    • 待機ブロック:実行状態のスレッドが wait() メソッドを実行すると、スレッドは待機ブロック状態に入ります。

    • 同期ブロック:スレッドが synchronized 同期ロックの取得に失敗した場合(同期ロックが他のスレッドに占有されているため)。

    • その他のブロック:スレッドの sleep() または join() の呼び出しによって I/O リクエストが発行されると、スレッドはブロック状態に入ります。sleep() のタイムアウト、join() によるスレッド終了待ちまたはタイムアウト、あるいは I/O 処理の完了により、スレッドは実行可能状態に戻ります。

  • 終了状態:

    実行状態のスレッドがタスクを完了するか、他の終了条件が発生すると、そのスレッドは終了状態に遷移します。


スレッドの優先度

すべての Java スレッドには優先度があり、これによりオペレーティングシステムがスレッドのスケジューリング順序を決定しやすくなります。

Java スレッドの優先度は整数で、その値の範囲は 1(Thread.MIN_PRIORITY)から 10(Thread.MAX_PRIORITY)です。

デフォルトでは、各スレッドには NORM_PRIORITY(5)の優先度が割り当てられます。

優先度の高いスレッドはプログラムにとってより重要であり、優先度の低いスレッドよりも先にプロセッサリソースを割り当てられる必要があります。ただし、スレッドの優先度はスレッドの実行順序を保証するものではなく、プラットフォームに大きく依存します。


スレッドの作成

Java にはスレッドを作成するための3つの方法があります:

  • Runnable インターフェースを実装する方法;
  • Thread クラス自体を継承する方法;
  • Callable と Future を使用してスレッドを作成する方法。

Runnable インターフェースを実装してスレッドを作成する

スレッドを作成する最も簡単な方法は、Runnable インターフェースを実装するクラスを作成することです。

Runnable を実装するには、クラスは run() メソッドの呼び出しを1つ実行するだけで済みます。宣言は次のとおりです:

public void run()

このメソッドはオーバーライドできます。重要なのは、run() が他のメソッドを呼び出し、他のクラスを使用し、変数を宣言できることを理解することです。メインスレッドと同じように。

Runnable インターフェースを実装するクラスを作成した後、その中でスレッドオブジェクトをインスタンス化できます。

Thread にはいくつかのコンストラクタが定義されていますが、次に示すものはよく使用されるコンストラクタです:

Thread(Runnable threadOb,String threadName);

ここで、threadOb は Runnable インターフェースを実装するクラスのインスタンスであり、threadName は新しいスレッドの名前を指定します。

新しいスレッドは、その start() メソッドを呼び出して初めて実行されます。

void start();

以下は、スレッドを作成して実行を開始する例です:

例

class RunnableDemo implements Runnable { private Thread t; private String threadName; RunnableDemo( String name) { threadName = name; System.out.println("Creating " + threadName ); } public void run() { System.out.println("Running " + threadName ); try { for(int i = 4; i > 0; i--) { System.out.println("Thread: " + threadName + ", " + i); //スレッドをしばらくスリープさせる Thread.sleep(50); } }catch (InterruptedException e) { System.out.println("Thread " + threadName + " interrupted."); } System.out.println("Thread " + threadName + " exiting."); } public void start () { System.out.println("Starting " + threadName ); if (t == null) { t = new Thread (this, threadName); t.start (); } } } public class TestThread { public static void main(String args[]) { RunnableDemo R1 = new RunnableDemo( "Thread-1"); R1.start(); RunnableDemo R2 = new RunnableDemo( "Thread-2"); R2.start(); } }

上記のプログラムをコンパイルして実行した結果は次のとおりです:

Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.

Thread を継承してスレッドを作成する

スレッドを作成する2つ目の方法は、Thread クラスを継承する新しいクラスを作成し、そのクラスのインスタンスを作成することです。

継承したクラスは run() メソッドをオーバーライドする必要があります。このメソッドは新しいスレッドのエントリポイントです。また、実行するには start() メソッドを呼び出す必要があります。

この方法はマルチスレッドの実装方法として挙げられていますが、本質的には Runnable インターフェースを実装したインスタンスでもあります。

例

class ThreadDemo extends Thread { private Thread t; private String threadName; ThreadDemo( String name) { threadName = name; System.out.println("Creating " + threadName ); } public void run() { System.out.println("Running " + threadName ); try { for(int i = 4; i > 0; i--) { System.out.println("Thread: " + threadName + ", " + i); //スレッドをしばらくスリープさせる Thread.sleep(50); } }catch (InterruptedException e) { System.out.println("Thread " + threadName + " interrupted."); } System.out.println("Thread " + threadName + " exiting."); } public void start () { System.out.println("Starting " + threadName ); if (t == null) { t = new Thread (this, threadName); t.start (); } } } public class TestThread { public static void main(String args[]) { ThreadDemo T1 = new ThreadDemo( "Thread-1"); T1.start(); ThreadDemo T2 = new ThreadDemo( "Thread-2"); T2.start(); } }

上記のプログラムをコンパイルして実行した結果は次のとおりです:

Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.

Thread のメソッド

次の表は、Thread クラスの重要なメソッドの一部を示しています:

番号 メソッドの説明
1 public void start()
このスレッドの実行を開始します。Java仮想マシンがこのスレッドの run メソッドを呼び出します。
2 public void run()
このスレッドが独立した Runnable 実行オブジェクトを使用して構築されている場合は、その Runnable オブジェクトの run メソッドが呼び出されます。それ以外の場合、このメソッドは何も実行せずに戻ります。
3 public final void setName(String name)
スレッド名を変更して、引数 name と同じにします。
4 public final void setPriority(int priority)
スレッドの優先度を変更します。
5 public final void setDaemon(boolean on)
このスレッドをデーモンスレッドまたはユーザースレッドとしてマークします。
6 public final void join(long millisec)
このスレッドが終了するまで最大 millis ミリ秒待機します。
7 public void interrupt()
スレッドを中断します。
8 public final boolean isAlive()
スレッドがアクティブかどうかをテストします。

上記のメソッドは Thread オブジェクトによって呼び出されます。次の表のメソッドは Thread クラスの静的メソッドです。

番号 メソッドの説明
1 public static void yield()
現在実行中のスレッドオブジェクトを一時停止し、他のスレッドを実行します。
2 public static void sleep(long millisec)
現在実行中のスレッドを指定されたミリ秒の間スリープ(一時停止)させます。この操作は、システムタイマーとスケジューラの精度と正確性の影響を受けます。
3 public static boolean holdsLock(Object x)
現在のスレッドが指定されたオブジェクト上でモニターロックを保持している場合に限り、true を返します。
4 public static Thread currentThread()
現在実行中のスレッドオブジェクトへの参照を返します。
5 public static void dumpStack()
現在のスレッドのスタックトレースを標準エラーストリームに出力します。

例

次の ThreadClassDemo プログラムは、Thread クラスのいくつかのメソッドを示しています。

DisplayMessage.java のコード:

//ファイル名 : DisplayMessage.java //Runnable インターフェースを実装してスレッドを作成する public class DisplayMessage implements Runnable { private String message; public DisplayMessage(String message) { this.message = message; } public void run() { while(true) { System.out.println(message); } } }

GuessANumber.java のコード:

//ファイル名 : GuessANumber.java //Thread クラスを継承してスレッドを作成する public class GuessANumber extends Thread { private int number; public GuessANumber(int number) { this.number = number; } public void run() { int counter = 0; int guess = 0; do { guess = (int) (Math.random() * 100 + 1); System.out.println(this.getName() + " guesses " + guess); counter++; } while(guess != number); System.out.println("** Correct!" + this.getName() + "in" + counter + "guesses.**"); } }

ThreadClassDemo.java のコード:

//ファイル名 : ThreadClassDemo.java public class ThreadClassDemo { public static void main(String [] args) { Runnable hello = new DisplayMessage("Hello"); Thread thread1 = new Thread(hello); thread1.setDaemon(true); thread1.setName("hello"); System.out.println("Starting hello thread..."); thread1.start(); Runnable bye = new DisplayMessage("Goodbye"); Thread thread2 = new Thread(bye); thread2.setPriority(Thread.MIN_PRIORITY); thread2.setDaemon(true); System.out.println("Starting goodbye thread..."); thread2.start(); System.out.println("Starting thread3..."); Thread thread3 = new GuessANumber(27); thread3.start(); try { thread3.join(); }catch(InterruptedException e) { System.out.println("Thread interrupted."); } System.out.println("Starting thread4..."); Thread thread4 = new GuessANumber(75); thread4.start(); System.out.println("main() is ending..."); } }

実行結果は次のとおりです。実行するたびに結果は異なります。

Starting hello thread...
Starting goodbye thread...
Hello
Hello
Hello
Hello
Hello
Hello
Goodbye
Goodbye
Goodbye
Goodbye
Goodbye
.......

Callable と Future によるスレッドの作成

  • 1. Callable インターフェースの実装クラスを作成し、call() メソッドを実装します。この call() メソッドはスレッドの実行本体として機能し、戻り値を持ちます。

  • 2. Callable 実装クラスのインスタンスを作成し、FutureTask クラスを使用して Callable オブジェクトをラップします。この FutureTask オブジェクトは、その Callable オブジェクトの call() メソッドの戻り値をカプセル化します。

  • 3. FutureTaskオブジェクトをThreadオブジェクトのtargetとして使用して、新しいスレッドを作成し開始します。

  • 4. FutureTaskオブジェクトのget()メソッドを呼び出して、子スレッドの実行終了後の戻り値を取得します。

例

public class CallableThreadTest implements Callable<Integer> { public static void main(String[] args) { CallableThreadTest ctt = new CallableThreadTest(); FutureTask<Integer> ft = new FutureTask<>(ctt); for(int i = 0;i < 100;i++) { System.out.println(Thread.currentThread().getName()+"のループ変数iの値"+i); if(i==20) { new Thread(ft,"戻り値のあるスレッド").start(); } } try { System.out.println("子スレッドの戻り値:"+ft.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } @Override public Integer call() throws Exception { int i = 0; for(;i<100;i++) { System.out.println(Thread.currentThread().getName()+" "+i); } return i; } }


スレッドを作成する3つの方法の比較

  • 1. Runnable、Callableインターフェースを実装する方法でマルチスレッドを作成する場合、スレッドクラスはRunnableインターフェースまたはCallableインターフェースを実装するだけであり、他のクラスを継承することもできます。

  • 2. Threadクラスを継承する方法でマルチスレッドを作成する場合、記述は簡単です。現在のスレッドにアクセスする必要がある場合、Thread.currentThread()メソッドを使用する必要はなく、直接thisを使用して現在のスレッドを取得できます。


スレッドのいくつかの主要な概念

マルチスレッドプログラミングでは、以下のいくつかの概念を理解する必要があります:

  • スレッド同期
  • スレッド間通信
  • スレッドデッドロック
  • スレッド制御:一時停止、停止、再開

マルチスレッドの使用

マルチスレッドを効果的に利用する鍵は、プログラムが直列に実行されるのではなく並行して実行されることを理解することです。例:プログラム内に並行して実行する必要がある2つのサブシステムがある場合、このときマルチスレッドプログラミングを利用する必要があります。

マルチスレッドを使用することで、非常に効率的なプログラムを作成できます。ただし、あまりにも多くのスレッドを作成すると、プログラムの実行効率は実際には向上せず、低下することに注意してください。

コンテキストスイッチのオーバーヘッドも重要であることを覚えておいてください。あまりにも多くのスレッドを作成すると、CPUがコンテキストスイッチに費やす時間は、プログラムの実行時間よりも多くなります。

その他の拡張