10
11

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のマルチスレッドを一からやって苦労したので整理する

Posted at

きっかけ

技術アップのため、ジャストシステムさんが作られた「Java100本ノック」をやった時のこと。
https://github.com/JustSystems/java-100practices/blob/master

マルチスレッド関連の問題で詰まりに詰まり、1問こなすのに2時間かかるとかいうひどい状態だった。
しかも色々ググってクリアしても色々よくわからない部分があったので、備忘録を兼ねて整理する。

昔授業でもやったけど、どう使えばいいのかすらよくわからなかったので記憶に残らなかった…

この記事では上記の100本ノックのマルチスレッドに関する問題を取り上げる。

おことわり

勉強中なので誤りがある可能性があります
なるべく誤ったことを書かないよう努力はしていますが、ここ違うよ!っていうコメントいただけるととても喜びます。

また、先述のJava100本ノックを見たことがある前提の記述が一部あります。ご了承ください。

Javaにおけるマルチスレッド基本

説明だとここがわかりやすかった。
https://eng-entrance.com/java-thread

Threadクラスを継承、もしくはRunnableインターフェースを実装したクラスの中で、run()メソッドを
オーバーライドしてマルチスレッドで行いたい処理を記述するというのが基本形。
(クラス定義しなくてもThread作れたりする…後述)

サンプルを作ってみる。
それぞれ違う文字列を標準出力に出すスレッドを2つ作って、並列で動かす。

ThreadSample.java
/**
 * 基本的なスレッド
 *
 */
public class ThreadSample extends Thread{
	// run()をオーバーライドして、その中に処理を書く
	@Override
	public void run() {
		for(int i = 0; i < 300; i++) {
			System.out.println("		マルチ-"+i);
		}
	}
}
ThreadSampleMain.java
/**
 * 基本的なスレッドの実行用クラス
 * ThreadSampleと並列実行する
 *
 */
public class ThreadSampleMain {

	public static void main(String[] args) {
		// Threadを継承したクラスをnewする
		ThreadSample thread = new ThreadSample();
		// .start()メソッドを実行すると、run()に書かれた処理が動く
		thread.start();
		for(int i=0; i < 300; i++) {
			System.out.println("メイン-"+i);
		}
	}

}

動かしてみるとこんな感じ。

(略)
メイン-297
		マルチ-0 # ここでMainが終わっていないのにもう片方のスレッドの内容が出ている
		マルチ-1
		マルチ-2
		マルチ-3
		マルチ-4
		マルチ-5
		マルチ-6
		マルチ-7
		マルチ-8
		マルチ-9
		マルチ-10
		マルチ-11
		マルチ-12
		マルチ-13
(略)

しかし、この一番単純なスレッドは、実行するごとにどこでThreadSampleの方のスレッドが実行されるかとか、そういうことが保証されていない。
これを保証できる仕組みが色々ある。

名前の宣言しなくてもスレッド使えるよ

さっきのコードでthreadと言う名前でThreadSampleのインスタンスを作ったけど、名前宣言しなくても使えるよパターン
Mainを書き換えるとこんな感じ。

ThreadSampleMain2
public class ThreadSampleMain2 {

	public static void main(String[] args) {
		// Threadを継承したクラスをnewしてそのまま開始
		new ThreadSample().start();
		for(int i=0; i < 300; i++) {
			System.out.println("メイン-"+i);
		}
	}
}

100本ノックの回答例を見ているとこういう記法が多い。
恥ずかしながらJavaウン年やっててnewしたオブジェクトにそのままメソッドの実行処理を書けるの知らなかった…

そもそもクラスにしなくても使えるよ

Java100本ノック問041の回答例などに見られるパターン

ThreadSampleMain3
public class ThreadSampleMain3 {

	public static void main(String[] args) {
		// newした無名のスレッドをそのまま実行する
		new Thread() {
			@Override
			public void run() {
				for(int i = 0; i < 300; i++) {
					System.out.println("		マルチ-"+i);
				}
			}
		}.start();

		for(int i=0; i < 300; i++) {
			System.out.println("メイン-"+i);
		}
	}

}

実際にエンタープライズ系のシステムで使われているのは今のところ見たことがないけど、こういう書き方もできるらしい。

Threadインスタンスの中にRunnableを入れる?

Java100本ノックの回答例を見ていると、Runnableを実装したクラスを直接start()せずに、Threadクラスのインスタンスの引数として代入して、そのインスタンスにstart()しているパターンがすごく多い。

例えば、以下、問040の回答例より引用

public class Answer040 implements Runnable {
    
    /**
     * 040の解答です.
     * キャッチされない例外のスタックトレースを
     * 現在時刻とともに標準エラー出力する.
     * 
     * @param arguments 使用しません.
     */
    public static void main(final String[] args) {
        Thread thread = new Thread(new Answer040());
        
        // UncaughtExceptionHandlerを実装したハンドラをsetUncaughtExceptionメソッドで登録.
        thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        
        // メインスレッドの実行.
        thread.start();
    }
    
    /**
     * スレッドを実行.
     */
    public void run() {
        // スリープ.
        try {
             Thread.sleep(500L);
        } catch (InterruptedException e) {
             e.printStackTrace();
        }
        
        Thread subThread = new Thread(new SubThread());
        
        // UncaughtExceptionHandlerを実装したハンドラをサブスレッドに紐付ける.
        subThread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        
        // サブスレッドの実行.
        subThread.start();
    }
}

この回答例を読んで疑問が浮かんだ。

  • なぜrun()をオーバーライドしているのか? subThread()を直接newして、それにstart()してはダメなのか?
  • run()の前にあるスリープはなんなのか?

例えば、こんな感じに書き換えても同じことができるはずでは?

public class Answer040-2 implements Runnable {

    public static void main(final String[] args) {
        // 注:SubThreadはRunnableをimplementsしたクラス
        SubThread sub = new SubThread();
        sb.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        sub.start();
    }

結論から言うとできません。(コンパイルエラーになる)

ThreadにRunnable実装インスタンスを登録する理由

先ほどのコードでいうとここ

public class Answer040 implements Runnable {

    /**
     * 040の解答です.
     * キャッチされない例外のスタックトレースを
     * 現在時刻とともに標準エラー出力する.
     * 
     * @param arguments 使用しません.
     */
    public static void main(final String[] args) {
        Thread thread = new Thread(new Answer040()); // ←ここ
()

そもそもThreadインスタンスの引数にRunnableを継承したインスタンスを入れるというのはどういう処理なのか、Javadocを見てみる。

public Thread(Runnable target)
新しいThreadオブジェクトを割り当てます。このコンストラクタは、Thread (null, target, gname) (gnameは新たに生成される名前)と同じ効果を持ちます。自動的に作成される名前は、nを整数とすると"Thread-"+nの形式を取ります。
パラメータ:
target - このスレッドの起動時に呼び出されるrunメソッドを含むオブジェクト。nullの場合、このクラスのrunメソッドは何も行わない。

なるほど。同じ作用をするコンストラクタがあると。そちらも見てみる。

public Thread(ThreadGroup group,
Runnable target,
String name)
その実行オブジェクトとしてtarget、名前として指定されたnameを持つ、groupによって参照されるスレッド・グループに属するような、新しいThreadオブジェクトを割り当てます。
セキュリティ・マネージャが存在する場合は、checkAccessメソッドがThreadGroupをその引数に指定して呼び出されます。
さらに、getContextClassLoaderまたはsetContextClassLoaderメソッドをオーバーライドしたサブクラスのコンストラクタから直接的または間接的に呼び出された場合、そのcheckPermissionメソッドがRuntimePermission("enableContextClassLoaderOverride")アクセス権で呼び出されます。

つまりThreadコンストラクタには、スレッドグループ(ThreadGroup)や、スレッド名(String)を省略できるコンストラクタがあり、先ほどのコードで触れたRunnableを引数として持つコンストラクタは、スレッドグループとスレッド名を自動割り当てにしている。

それで、このコンストラクタは何をしているかと言うと、

新しいThreadオブジェクトを割り当てます。

ということらしい。
Threadオブジェクトを割り当てますとはどういうことなのか調べてみる。
https://www.task-notes.com/entry/20151031/1446260400

上記サイトによるとThreadクラスは引数として渡されたRunnableオブジェクトのrun()を動作させるような実装になっている。
すなわち引数のRunnableに自作のスレッドを渡すというのは、これをrun()で実行してねと言うこと。
また、extends Threadよりも(run()以外をオーバーライドしない限り)implements Runnableの方が良いことがわかった。
恐らく、多重継承できないためextendsは控えた方がいいのもあるだろう。

これらから、今の時点で得られた結論としては直接newせずThreadに対して渡すのはjava.lang.Threadのスーパークラスのrun()メソッドを利用するためだということと、後述するスレッドグループやExceptionHandlerのメソッドの都合だと推測している。

サブスレッド生成前にスリープする理由

先ほどの回答例のここ


/**
     * スレッドを実行.
     */
    public void run() {
        // スリープ.
        try {
             Thread.sleep(500L); // ←ここ
        } catch (InterruptedException e) {
             e.printStackTrace();
        }
        
        Thread subThread = new Thread(new SubThread());
        
        // UncaughtExceptionHandlerを実装したハンドラをサブスレッドに紐付ける.
        subThread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        
        // サブスレッドの実行.
        subThread.start();
    }

この処理はたびたび出てきて、職場の先輩も「なぜかわからんけどこれ入れないと上手く動かない」と言っていた。
色々ググってみたが出てこず。

mainメソッドに直接書かれたThreadがstart()された後、一瞬待たないとsetUncaughtExceptionHandler()が成功しないことがあるからでは? と推測している。
ひとまず、今のところはおまじないとして覚えておくことに。

余談:setUncaughtExceptionHandler()について

これは先ほどのJava100本ノック問040で使用する必要のあるメソッド。
UncaughtExceptionHandlerというインターフェースを実装したクラスを作っておき、その中でuncaughtException(Thread,Trowable)をオーバーライドしておくと、catchできなかった例外をスレッドが吐いたとき、スレッドが異常終了する直前にこのメソッドが呼ばれる。
例えば例外のログ出力などに役立つ
参考:https://javazuki.com/articles/uncaught-exception-handler-usage.html

そしてこれは、Java.lang.Threadのメソッドである。Runnableには無いので、Threadに対してRunnable実装のインスタンスを入れて使うのがいいようだ。
先ほどの章でいきなりsubThreadクラスをnewして出来ないのは、ないメソッドを呼ぼうとしていたのでコンパイルエラーになっていた。

スレッドグループへの登録

スレッドグループ:スレッドをグループ化して、アクティブ数の監視や複数スレッドの一時停止などができる仕組み
スレッドグループを使うと、登録時にスレッドに名前を設定できて、スレッドグループの中の名前というスコープで管理できるので、普通の変数と混ざらず良い感じらしい。

100本ノック問35の答えなどで出てくる記法
https://github.com/JustSystems/java-100practices/blob/master/contents/035/README.md

以下、問35の答えより引用

public final class Answer035 implements Runnable {
    /* スレッドグループA. */
    private static ThreadGroup groupA = new ThreadGroup("GroupA");
    
    /* スレッドグループB. */
    private static ThreadGroup groupB = new ThreadGroup("GroupB");
    
    /**
     * グループA,Bのスレッドを各100スレッド実行する.
     */
    @Override public void run() {
        for (int i = 0; i < 100; i++) {
            new Thread(groupA, new ThreadRun(), "thread" + i).start();
        }
        
        for (int i = 0; i < 100; i++) {
            new Thread(groupB, new ThreadRun(), "thread" + i).start();
        }
    }
    
    /**
     * 各スレッドグループにおけるアクティブスレッド数を出力する.
     *
     * @param point カウント回数
     */
    public static void printActiveCount(int point) {
        System.out.println("Active Threads in Thread Group " + groupA.getName() +
            " at point(" + point + "):" + " " + groupA.activeCount());
    
        System.out.println("Active Threads in Thread Group " + groupB.getName() +
            " at point(" + point + "):" + " " + groupB.activeCount());
    }
    
    /**
     * 035の解答です.
     * スレッドグループごとにスレッドを実行し、
     * 各スレッドにてアクティブであるスレッド数を標準出力する.
     *
     * @param arguments 使用しません.
     */    
    public static void main(String[] args) throws InterruptedException {
        /* 新しいスレッドを割り当てる. */
        Thread thread = new Thread(new Answer035());
        
        /* スレッドを実行する. */
        thread.start();
        
        // アクティブスレッド数を出力する.
        for (int i = 1 ;; i++) {
            printActiveCount(i);
            thread.sleep(1000L);
            
            // アクティブスレッドが0になったときにループを抜ける.
            if (groupA.activeCount() == 0 && groupB.activeCount() == 0) {
                break;
            }
        }
    }
}

これのrun()のオーバーライドの部分。
これは、スレッドグループに、自作のThread継承クラスThreadRunを登録しようとしている。
Threadグループで管理するときは、Threadクラスのコンストラクタのうち、第二引数にThreadを取れるものを使って、そこにnewすることで複数個のスレッドをいちいち名前宣言しなくても使える。

スレッド同士での排他処理

複数のスレッドで、この処理はこの処理より前にやったら困るって時とかに使う。
色々やり方があって、この辺から一気に複雑になるので、やり方別にまとめていく

用語整理

スレッドセーフ

ある処理と、ある処理は並列実行しても問題を起こさないよという意味
「スレッドセーフなメソッド」みたいな感じ。

さっきのように全然関係のない変数を出力するだけならいいけど、static変数を参照するメソッドと書き換えるメソッドが一緒にあって、書き換える前に参照したりしてNullPointerException吐いたりとかするのはスレッドセーフじゃない。

スレッドセーフにさせるための処理方法が色々ある。1つ実行してる間はもう片方を止めるとか。
参考元:https://wa3.i-3-i.info/word12456.html

スレッドプール

スレッドを新しく作るより、使いまわした方が早いため、生成済みのスレッドを使いまわす手法のこと。
スレッドプールの生成はExecutor(ExecutorService)で行う。

synchronized使うパターン

javaのメソッド宣言に"synchronized"というものをつけて書いた処理。
これが付いたメソッド同士は、同時に実行されなくなりnotify()やwait()などでメソッドで実行を制御することができる。
自分語りだけど、いい加減な勉強していた学生時代でもこの2つだけは覚えていたので、「マルチスレッドとかsynchronizedすればええやん」とかいう頭の悪いことを言っていた苦い記憶がある。

Java100本ノックでもこれは出てくるが、やってみるとはまったのでメモ。

やってはいけないパターン

Java100本ノック問041より

wait()とnotify()を用いて、「1から10000までの整数を加算して結果をグローバル変数に格納する」スレッドAと、「スレッドAの動作終了後グローバル変数の値を標準出力に出力する」スレッドBと、スレッドA,Bをほぼ同時に開始するプログラムとを実装せよ。

スレッドA:Q41_ThreadA.java
スレッドB:Q41_ThreadB.java
処理2つとグローバル変数のクラス:Q41_number.java
実行用:Q41.java
として以下のように実装した。

Q41_number
public class Q41_number {

	public static long number = 0;

	public synchronized void addNumber() {
		System.out.println("add number...");
		for(long i = 1; i <= 10000; i++) {
			number += i;
		}
		System.out.println("end");
		// 終了したことを通知する
		notify();
	}

	public synchronized void showNumber() {
		try {
			System.out.println("waiting...");
			wait();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(number);
	}
}
Q41_ThreadA
public class Q41_ThreadA implements Runnable{
	Q41_number q41 = new Q41_number();

	@Override
	public void run() {
		// wait()する前にnotify()すると無限ループするので、少し待つ
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
		q41.addNumber();
	}
}
Q41_ThreadB
public class Q41_ThreadB implements Runnable{
	Q41_number q41 = new Q41_number();

	@Override
	public void run() {
		// ThreadAより僅かに先に動くようにする
        try {
            Thread.sleep(500L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
		q41.showNumber();
	}
}
Q41
public class Q41 {

	public static void main(String[] args) {
		Thread threadA = new Thread(new Q41_ThreadA());
		Thread threadB = new Thread(new Q41_ThreadB());
		// 実行する
		threadA.start();
		threadB.start();
	}
}

実行するとQ41_number#showNumber()のwait()のところで止まって動かなくなる

理由

http://www.ne.jp/asahi/hishidama/home/tech/java/thread.html
このサイトのsynchronizedの章のコメントに書いていた。

//↑同一インスタンスのfunc1()とfunc2()を別スレッドから同時に呼び出しても、片方ずつしか実行されない。
// 同一インスタンスのfunc1()とfunc1()を別スレッドから呼び出すのも同様。
// 別インスタンスであれば、ロックオブジェクトが異なるので排他されず、同時に実行される。

さっきのコードだと、ThreadAとThreadBが持つQ41_numberのインスタンス自体が違う。
synchronizedなメソッドを持つインスタンスは複数のスレッドで共用するようにすること。

volatileについて

フィールドの定義時に設定できる。
これをつけると、そのフィールドはコンパイラの最適化の時にもキャッシュされなくなる。
ふわふわした言い方だけど、「このフィールド別スレッドが書き換えてるはずなのにデバッグしてみたら書き換わってないんだけど…」と言う時にキャッシュが使用されていることを疑ってみる。

キャッシュやそれによっておこる現象は以下のサイトが詳しい。
https://www.sejuku.net/blog/65848

Java100本ノックだと問017がこれを使っている。

スレッド単位での実行順定義や終了待機

複数個のスレッドがあって、それらにスレッド単位で実行順をつけたりする時に役立つ
java.util.concurrent.CountDownLatch を使う。
これは定義した時に設定する数値がゼロになるまで待機するawait()と、数値を1減らすcountDownLatch()を利用して、すべてのスレッドが終わるまで待機などを実現する。

10
11
0

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
10
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?