2020年9月6日日曜日

3: 'wait'/'notify'/'notifyAll'を用いたJavaスレッド群同期

<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>

Javaにおける'wait'、'notify'、'notifyAll'を用いたスレッド群同期について、いくつかの注意点があり、それによって、一部のつまずきを避けられるかもしれません。

話題


About: Java

この記事の目次


開始コンテキスト


  • 読者は、Javaプログラミング言語の基本的知識を持っている。

ターゲットコンテキスト



  • 読者は、Javaにおける'wait'、'notify'、'notifyAll'を用いたスレッド群同期について、いくつかの注意点を知る。

オリエンテーション


Hypothesizer 7
私はJavaにおける'wait'、'notify'、'notifyAll'を用いたスレッド群同期でつまずきがちだ。

それは、あなたには関係ない私の問題ですか?. . .おめでとうございます!. . .しかし、私が推測するに、もしもそうならば、あなたはこれを読んでいないでしょう。

'wait'を用いないスレッド群同期は全然問題でないと私は言い切る: それは、ただ、単一のスレッドだけがガードされたブロック群に入れるというだけのことだ。

しかしながら、'wait'が関わると、状況はもっと複雑になり、私はつまずきがちになる。

基本的には、それは不注意であるという問題にすぎないが、次回はもっと注意深くすると誓うというのは、効果的な戦略ではない、私の経験によれば。

そこで、スレッド群同期をするときに向かうべき参照文献として、いくつかの注意点を記録するべきだ、と私は判断した。


本体


1: ロックとモニターの関係


Hypothesizer 7
まず、ロックとモニターの関係を明確にしよう。

注意しておくが、私はJavaロックとJavaモニターについてのみ話す。より一般的なコンセプトとして、ロックおよびモニターは、本記事で記述されるようではないかもしれない。

それを明確にするのは、一部の人々によるそれらの用語の謝った使用を指摘することを私が楽しく思うからではなく、それが、スレッド群同期の振る舞いを明確にするのに助けになるからである。

ロックは、単一スレッドのみが保有できる2値セマフォアである。

それでは、なぜ、モニターが必要とされるのか?. . .それは、ただの裸のロックでは、スレッド群を同期するのに十分でないから。

1つには、ロックを獲得することを期待しているスレッド群がとどまらなければならないリストがなければならない('待っている'という用語を使用しなかったことに注意)。そのリストを'期待しているスレッド群リスト'と私は呼ぶ(広く受け入れられた用語ではない)。

また、ロックを獲得することを待っているスレッド群がとどまらなければならない別のリストもなければならない('待つ'という用語を、広い意味にではなく、特定の意味において、私は使用する)。そのリストを'待っているスレッド群リスト'と私は呼ぶ(広く受け入れられた用語ではない)。

ここでの'待つ'は、'wait'メソッドを呼んで待ち時間(もしも指定されていれば)が経過してないことを意味している。つまり、待っているスレッドは、本当にはロックを獲得することを期待しておらず、期待しているスレッド群リストに移るように通知を受けることを期待しているのだ。

ここで、任意の待っているスレッドはまず、期待しているスレッド群リストへ移り、その後にロックを獲得するもの、と私は仮定した。実際には、実装上は、1つの待っているスレッドは、期待しているスレッド群リストへ移る労をとらずに直接にロックを獲得するということもありえるかもしれないが、少なくとも、'notifyAll'によって通知を受けた、待っている全てのスレッド群の内の他のものたちは、期待しているスレッド群リストへ移らなければならず(待っている全てのスレッド群の内の1つだけが直接にロックを獲得できるから)、コンセプト上は、待っている全てのスレッド群がまず、期待しているスレッド群リストへ移るものと考えてよいだろう(もしも、それらの内の1つが実際にはその部分を省くとしても)。

そこで、スレッドには3つの状態があることを知ることが重要だ: ロックを保有している(同時には単一のスレッドだけがロックを保有することができる)、ロックを獲得することを期待している、ロックを獲得することを期待することができるようになるのを待っている。

モニターとは何かということに戻ると、モニターとは、関係スレッド群を上記3状態に管理する構造である。

実際には、「私はモニターを使い、モニターがロックを使う」と言うべきか、「私はモニターおよびロックを使う」と言うべきか、私はよく知らないのだが、その違いは、Java標準ライブラリ群の実装の問題であり、どちらであろうが、ユーザーには何の影響もないであろう。


2: モニターとオブジェクトの関係


Hypothesizer 7
モニター(ロックと共に)は、'Object'クラス内に組み込まれている。

しかしながら、ガードされたブロック群内でガードされているものは、必ずしも、そのモニターのオブジェクトの状態ではない。

謎めいている?1つの例を見てみよう。

@Java ソースコード
public class ClassA {
	private ClassB i_classB = new ClassB ();
	private int i_integer = 0;
	
	public void synchronizedNoWaitingNoNotifyingMethod (int a_threadIdentification, int a_sleepTime) throws InterruptedException {
		System.out.println (String.format ("### the thread -> '%d' before the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
		System.out.flush ();
		synchronized (i_classB) {
			i_integer ++;
			System.out.println (String.format ("### the thread -> '%d' in     the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
			System.out.flush ();
			Thread.sleep (a_sleepTime);
		}
		System.out.println (String.format ("### the thread -> '%d' after  the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
		System.out.flush ();
	}
	
	public void synchronizedWaitingMethod (int a_threadIdentification, int a_sleepTime) throws InterruptedException {
		System.out.println (String.format ("### the thread -> '%d' before the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
		System.out.flush ();
		synchronized (i_classB) {
			i_integer ++;
			System.out.println (String.format ("### the thread -> '%d' in     the guarded block before waiting: 'i_integer' -> '%d'", a_threadIdentification, i_integer));
			System.out.flush ();
			i_classB.wait ();
			System.out.println (String.format ("### the thread -> '%d' in     the guarded block after  waiting: 'i_integer' -> '%d'", a_threadIdentification, i_integer));
			System.out.flush ();
			Thread.sleep (a_sleepTime);
		}
		System.out.println (String.format ("### the thread -> '%d' after  the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
		System.out.flush ();
	}
	
	public void synchronizedNotifyingMethod (int a_threadIdentification, int a_sleepTime) throws InterruptedException {
		System.out.println (String.format ("### the thread -> '%d' before the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
		System.out.flush ();
		synchronized (i_classB) {
			i_integer ++;
			System.out.println (String.format ("### the thread -> '%d' in     the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
			System.out.flush ();
			i_classB.notifyAll ();
			Thread.sleep (a_sleepTime);
		}
		System.out.println (String.format ("### the thread -> '%d' after  the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
		System.out.flush ();
	}
}

public class ClassB {
}

public class Test1Test {
	private Test1Test () {
	}
	
	public static void main (String [] a_arguments) throws Exception {
		Test1Test.test ();
	}
	
	public static void test () throws Exception {
		ClassA l_classA = new ClassA ();
		Thread l_subThread0 = new Thread (() -> {
			try {
				l_classA.synchronizedNoWaitingNoNotifyingMethod (0, 10000);
			}
			catch (Exception l_exception) {
				l_exception.printStackTrace ();
			}
		});
		Thread l_subThread1 = new Thread (() -> {
			try {
				l_classA.synchronizedNoWaitingNoNotifyingMethod (1, 0);
			}
			catch (Exception l_exception) {
				l_exception.printStackTrace ();
			}
		});
		Thread l_subThread2 = new Thread (() -> {
			try {
				l_classA.synchronizedWaitingMethod (2, 0);
			}
			catch (Exception l_exception) {
				l_exception.printStackTrace ();
			}
		});
		Thread l_subThread3 = new Thread (() -> {
			try {
				l_classA.synchronizedWaitingMethod (3, 0);
			}
			catch (Exception l_exception) {
				l_exception.printStackTrace ();
			}
		});
		Thread l_subThread4 = new Thread (() -> {
			try {
				l_classA.synchronizedNotifyingMethod (4, 0);
			}
			catch (Exception l_exception) {
				l_exception.printStackTrace ();
			}
		});
		Thread l_subThread5 = new Thread (() -> {
			try {
				l_classA.synchronizedNoWaitingNoNotifyingMethod (5, 0);
			}
			catch (Exception l_exception) {
				l_exception.printStackTrace ();
			}
		});
		l_subThread0.start ();
		Thread.sleep (1000);
		l_subThread5.start ();
		Thread.sleep (1000);
		l_subThread4.start ();
		Thread.sleep (1000);
		l_subThread3.start ();
		Thread.sleep (1000);
		l_subThread2.start ();
		Thread.sleep (1000);
		l_subThread1.start ();
		l_subThread0.join ();
		l_subThread1.join ();
		l_subThread2.join ();
		l_subThread3.join ();
		l_subThread4.join ();
		l_subThread5.join ();
	}
}

確かに、それは不自然だ: なぜ、'i_classB'の代わりに'this'を使わないのか?しかし、それはとにかく、ガードされたブロック群内でガードされているものは、'ClassA'インスタンスの状態であり、'i_classB'(それのモニターが使われている)の状態ではない: 'i_classB'は、純粋にモニタリング目的で使用されている。

そして、私は、上記例において、'i_classB.wait ()'等を使用しなければならない、単に'wait ()'(それは、'this.wait ()'を意味する)ではなく。


3: ガードされたブロック群内のスレッド数


Hypothesizer 7
「同時には、ただ1つのスレッドだけが、ガードされたブロック群の中に入れる」という考えは捨て去らなければならない。

その代わりにこれを採用しなければならない: 「同時には、ただ1つのスレッドだけが、ガードされたブロック群の中で動作できる」。

実際、あるスレッドが、あるガードされたブロックの中に入って待てば、別のスレッドは、任意のガードされたブロックの中に入ることができ、そこで待つこともできる。したがって、任意の数のスレッドが、ガードされたブロック群内にいることができる。

しかしながら、ガードされたブロック群内でただ1つのスレッドだけが本当に動作していることは、確信できる。


4: 待ち後の状態変化


Hypothesizer 7
これは、注意深く考えれば明白なのだが、私にとって、お決まりのつまずきの石であるようだ: 待ちに入る前の状態認識は、待ち後にはもはや有効ではない。

スレッドが待ちに入った後、ガードされたブロック群の中にいくつかの他のスレッドたちが入って状態を変更したかもしれない(実のところ、通常、それが待つ目的だ)。

したがって、再開するスレッドは、今や新たな世界秩序の中にいるものと想定し、先入観のない目で世界を見回さなければならない。


5: 'notify'または'notifyAll'が行なうこと


Hypothesizer 7
'notify'または'notifyAll'は1つの通知を受けたスレッドを動作させ始める、と想定することはできない。

第1に、'notify'や'notifyAll'はロックをリリースしない、したがって、通知を行なったスレッドが、ガードされたブロック群を去るまで、いかなる、通知を受けたスレッドも、動作し始める可能性は全然ない。

第2に、通知したスレッドが、ガードされたブロック群を去った後でも、その通知したスレッドは、シングルコアシングルCPUシステムにて動作し続けるかもしれない、だって、なぜ、そうであってはいけないのか?または、通知を受けたのではない、期待しているスレッドが、ロックを獲得するかもしれない、ある通知を受けたスレッドがではなく。

私の理解によれば、通知するということは、ただ、1つの待っているスレッドまたは全ての待っているスレッド群を、期待しているスレッド群リストへ移すことを意味し、通知を受けなかったものを含む期待しているスレッド群リスト内のどのスレッドがロックを与えられるかは、別問題である。

実際、以下が、あるシングルコアシングルCPU Linuxコンピュータにおける上記コードの1つの結果だ(それは、勿論、別の環境またはタイミングによっては異なるものになるかもしれない)。

@出力
### the thread -> '0' before the guarded block               : 'i_integer' -> 0.
### the thread -> '0' in     the guarded block               : 'i_integer' -> 1.
### the thread -> '5' before the guarded block               : 'i_integer' -> 1.
### the thread -> '4' before the guarded block               : 'i_integer' -> 1.
### the thread -> '3' before the guarded block               : 'i_integer' -> 1.
### the thread -> '2' before the guarded block               : 'i_integer' -> 1.
### the thread -> '1' before the guarded block               : 'i_integer' -> 1.
### the thread -> '0' after  the guarded block               : 'i_integer' -> 1.
### the thread -> '1' in     the guarded block               : 'i_integer' -> 2.
### the thread -> '2' in     the guarded block before waiting: 'i_integer' -> 3.
### the thread -> '3' in     the guarded block before waiting: 'i_integer' -> 4.
### the thread -> '4' in     the guarded block               : 'i_integer' -> 5.
### the thread -> '1' after  the guarded block               : 'i_integer' -> 2.
### the thread -> '4' after  the guarded block               : 'i_integer' -> 5.
### the thread -> '5' in     the guarded block               : 'i_integer' -> 6.
### the thread -> '5' after  the guarded block               : 'i_integer' -> 6.
### the thread -> '3' in     the guarded block after  waiting: 'i_integer' -> 6.
### the thread -> '2' in     the guarded block after  waiting: 'i_integer' -> 6.
### the thread -> '3' after  the guarded block               : 'i_integer' -> 6.
### the thread -> '2' after  the guarded block               : 'i_integer' -> 6.

その結果が意味するには、1) スレッド'0'が、ガードされたブロック群の中に入り、そこに長時間とどまった 2) その間に、スレッド'5'、'4'、'3'、'2'、'1'が、その順序で、期待しているスレッド群リストに入った 3) スレッド'0'がガードされたブロック群を去った 4) スレッド'1'が、ガードされたブロック群の中に入って去った(スレッド'1'に対する"after the guarded block"というメッセージは遅れるが、スレッド'1'は、ガードされたブロック群を即座に去ったはずだ(それは、'i_integer'の値によって証明されている)) 5) スレッド'2'が、ガードされたブロック群の中に入って待ち始めた 6) スレッド'3'が、ガードされたブロック群の中に入って待ち始めた 7) スレッド'4'が、ガードされたブロック群の中に入って、スレッド'2'および'3'に通知をして去った 8) スレッド'5'が、ガードされたブロック群の中に入って去った 9) スレッド'3'が、ガードされたブロック群を去った 10) スレッド'2'が、ガードされたブロック群を去った。

えーと、メッセージ出力実行の順序と表示されたメッセージの順序との関係は不確かなところがある("the thread -> '1' after the guarded block"が遅れて来るように)が、スレッド'5'が、通知を受けたスレッド'2'および'3'より先に実行再開したことは、間違いない、なぜなら、通知を受けたスレッドたちは、スレッド'5'によって変更された状態を見たから。

余談だが、予期に反して、スレッド群は、ロックを、FIFO順ではなくLIFO順にて与えられる傾向にあるようだ、私の環境では。


6: 結びとその先


Hypothesizer 7
これで、スレッド群同期をコーディングする際に向かうべきいくつかの注意点を私は手にした。

スレッド群同期に関係したバグはデバッグに時間を多く要しがちなので、始めからコードを正しく書くことがとても有益だろう。

いくつかの他のJava機能に対しても、そうした注意点を記録していこう、以降の記事にて。


参考資料


<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>