スレッドの基礎

みるくあいらんどっ! > ドキュメント > Java > じっくり学ぶ Java講座 [初心者向け・入門]


シングルスレッドとマルチスレッド

シングルスレッドとマルチスレッド

今まで作成したプログラムは、すべて mainメソッドから実行され、順番に処理が行われていました。プログラムを実行している箇所は 1箇所しかありませんでした。このようなプログラムを、シングルスレッドと呼びます。

一方、マルチスレッドなプログラムでは、プログラムを実行している箇所が複数箇所あります。本章では、mainスレッドともう 1つのスレッドの、合計 2つのスレッドを扱うプログラムについて、解説していきます。

マルチスレッドの利点

マルチスレッドの利点は、「何かをしながら別の何かをする」ことができるようになることです。例えば、ウェブブラウザでは、通信環境が遅い場合にはまず画像無しでページを表示します。そのページを表示しながら、画像を読み込む処理を行っています。

また、例えばゲーム機のゲームソフトの場合には、「Now Loading」の間に、データを読み込みながら画面にアニメーションを描画するようなことがあります。これもマルチスレッド的な処理と言えます。

マルチスレッドの欠点

マルチスレッドの欠点は、ソースコードを追うことがとても難しくなることです。処理が実行される順番が非常に複雑になります。スレッドを管理するためのプログラムの記述が難しいです。また、バグのあるプログラムを製作してしまった場合、タイミングによってバグが発生したり発生しなかったりします。そのため、バグの原因追及やデバッグに非常に手間どることがあります。

マルチスレッドとマルチプロセス

マルチスレッドと似た言葉に、マルチプロセスがあります。

マルチプロセスは、基本的にプロセス同士に相互関係はありません。例えば、ウェブブラウザを 3個起動しているような状態です。

一方、マルチスレッドは、ひとつのプロセスに複数のスレッドが存在します。例えば、ウェブブラウザを 1個起動しており、そのタブブラウザ機能を用いて、3つのウェブサイトを表示しているような状態です。

同期メソッドと非同期メソッド

同期メソッドと非同期メソッド

今まで利用してきたメソッドは、すべて同期メソッドでした。同期メソッドでは、メソッドの呼び出しの後、そのメソッドの内容を実行し終えた後に、次の処理に移ります。

これに対して、非同期メソッドの場合には、メソッドの呼び出しの後、新しいスレッドが作られます。続いて、そのメソッドの内容が実行されるのか、それとも、メソッドを呼び出した側の次の処理が行われるのかは、基本的には運任せになります。Javaでは、非同期メソッドを呼び出すことで、マルチスレッドを実現します。

同期メソッドを利用する

今まで利用してきたメソッドはすべてが同期メソッドでした。ここでは Point型インスタンスの translateメソッドを利用した例を挙げてみます。

ソースコードは以下の通り。

Q101/Q101.java

import java.awt.Point;

/**
 * 同期メソッドを確認します。
 */
public class Q101 {
	
	/**
	 * メインメソッド。
	 * @param args 引数
	 */
	public static void main(String[] args) {
		Point point = new Point(10, 20);
		
		// 移動させる(このメソッドは同期メソッド)
		point.translate(50, 100);
		
		System.out.println("変数 pointの参照先インスタンスの内容は" + point);
	}
}

実行結果の例は以下の通り。

変数 pointの参照先インスタンスの内容はjava.awt.Point[x=60,y=120]

当然の結果となっています。16行目にてメソッド呼び出しが行われ、translateの処理が実施された後、18行目が実行されています。

イメージとしては以下のような感じです。

同期メソッド

非同期メソッドを利用する

自作ライブラリ AsyncPointを使用して、非同期メソッドの動作確認をします。まずは、自作ライブラリ AsyncPointをダウンロードしてください。AsyncPointクラスは Pointクラスと似たクラスですが、translateメソッドは非同期メソッドとして実装されています。

ソースコードは以下の通り。

Q102/AsyncPoint.java(ライブラリをそのまま利用します)

Q102/Q102.java

/**
 * 非同期メソッドを確認します。
 */
public class Q102 {
	
	/**
	 * メインメソッド。
	 * @param args 引数
	 */
	public static void main(String[] args) {
		AsyncPoint point = new AsyncPoint(10, 20);
		
		// 移動させる(このメソッドは非同期メソッド)
		point.translate(50, 100);
		
		System.out.println("変数 pointの参照先インスタンスの内容は " + point);
	}
}

実行結果は以下の通り。

変数 pointの参照先インスタンスの内容は AsyncPoint(60,120)
変数 pointの参照先インスタンスの内容は AsyncPoint(10,20)

このプログラムを何度も何度も実行すると、前者の実行結果になる場合と、後者の実行結果になる場合があります。僕のパソコン・環境の場合には、前者となる確率が 9割くらい、後者となる確率が 1割くらいになっています。お使いのパソコンの性能や、使用している Java環境によって、この確率は異なったものになります。環境によっては、前者または後者のどちらかの実行結果しか実現しないかもしれません。

さて、非同期メソッド translateを利用したことで、どうして実行結果が異なったものになるのでしょうか。

まずは前者について考えてみます。

イメージとしては以下のような感じです。

非同期メソッド (1)

translateメソッドが呼び出され、その内容が実行された後、インスタンスの内容が表示されています。

続いて後者について考えてみます。

イメージとしては以下のような感じです。

非同期メソッド (2)

translateメソッドが呼び出されたものの、translateメソッドの内容が実行される前に、インスタンスの内容が表示されています。

AsyncPointクラスの translateメソッドは非同期メソッドのため、このように実行結果が 2種類に分かれます。マルチスレッドなプログラムは、思わぬ結果を招く。マルチスレッドの難しさが垣間見えたでしょうか。

Javaの非同期メソッド

Javaで、代表的な非同期メソッドのには以下のものがあります。これらのメソッドを利用すると、マルチスレッドなプログラムになります。この章では、Threadクラスに解説します。本ウェブサイトでは TimerTaskについては解説しません。なお、先の AsyncPointでは、内部で Threadクラスを利用しています。

  • Threadクラスの startメソッド
  • Timerクラスの scheduleメソッド
  • Timerクラスの scheduleAtFixedRateメソッド

これらのクラスについて詳細が気になる方は、説明書(API)を参照してください。Java SE 7(Java 1.7)の説明書(API)を掲示しておきます→ThreadクラスTimerクラス

スレッドの本題に入る前に

インスタンスの作成と executeメソッド

mainメソッドは staticなメソッドなので、インスタンスを生成せずに実行されています。ところが、この章と次の章では、インスタンスでないと都合が悪いという状況が幾つか発生します。具体的には予約語thisを使用したいという事情があります。そこで、ここではメインメソッドからインスタンスを生成し、インスタンスメソッド executeを実行するサンプルを挙げます。

ソースコードは以下の通り。

Q103/Q103.java

/**
 * インスタンス生成を確認します。
 */
public class Q103 {
	
	/**
	 * メインメソッド。
	 * @param args 引数
	 */
	public static void main(String[] args) {
		// 自クラスのインスタンスを生成する
		Q103 q103 = new Q103();
		// インスタンスの executeメソッドを実行する
		q103.execute();
	}
	
	/**
	 * 実行します。
	 */
	public void execute() {
		System.out.println("Hello, world!");
	}
}

実行結果は以下の通り。

Hello, world!

12行目で Q103型インスタンスを生成しています。そして、14行目でインスタンスメソッド executeを呼び出しています。その結果、21行目が実行されていることが分かります。

現在のスレッド名を取得する

この章のマルチスレッドでは、複数のスレッドが動作します。スレッドの名前を確認することできれば理解が早まるかもしれません。実行したスレッドの名前を表示するためのサンプルを挙げます。

ソースコードは以下の通り。

Q104/Q104.java

/**
 * 現在のスレッドを確認します。
 */
public class Q104 {
	
	/**
	 * メインメソッド。
	 * @param args 引数
	 */
	public static void main(String[] args) {
		Thread currentThread = Thread.currentThread();
		String name = currentThread.getName();
		System.out.println("現在のスレッドは" + name + "です。");
		
		// 1行で表現したもの
		System.out.println("現在のスレッドは" + Thread.currentThread().getName() + "です。");
	}
}

実行結果は以下の通り。

現在のスレッドはmainです。
現在のスレッドはmainです。

mainメソッドは、「main」という名前のスレッドによって実行されていることが分かります。

Threadクラスの sleepメソッド

Threadクラスの staticなメソッドである sleepメソッドを使用することで、スレッドの動きを一時的に止めることができます。具体的な例は以下です。

ソースコードは以下の通り。

Q105/Q105.java

/**
 * Thread.sleepメソッドを確認します。
 */
public class Q105 {
	
	/**
	 * メインメソッド。
	 * @param args 引数
	 */
	public static void main(String[] args) {
		System.out.println("プログラムを起動しました。");
		System.out.println("3秒待ちます。");
		
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			; // 何もしない
		}
		
		System.out.println("3秒経過しました。");
		System.out.println("プログラムを終了します。");
	}
}

実行結果は以下の通り。

プログラムを起動しました。
3秒待ちます。
3秒経過しました。
プログラムを終了します。

実行すれば、プログラムが 3秒(3000ミリ秒)停止することが分かると思います。

なお、sleepメソッドは例外 InterruptedExceptionを発生する可能性があります。この例外は非ランタイム系例外です。そのため、この例外が実際に発生するかはともかくとして、メソッド内で catchするか、それともメソッド外へ throwsするかを明示しなければなりません。本ウェブサイトでは、catchするものの、特段何もしないという処理を記述することにします。

最終更新: 2014/08/01 , 公開: 2013/03/04
▲top