オブジェクト指向プログラミングb 第8回


●スレッド「もっとJavaを学ぶ 第6回 (2004年2月 )」

 Javaでは,スレッドという単位で並行処理を手軽に行うことができる。今回は,スレッ
ドの基本事項を解説する。スレッドを適切に使用すると,CPUのパワーを無駄なく利用で
き,プログラムの構造もより明確になる。

【スレッドとは】
 私たちが使用しているコンピュータ上では,複数のプログラムが同時に動作している。
たとえば,学生諸君も日常的にWebブラウザやメールソフト,ワープロソフト,表計算ソ
フトなどを同時に起動して,それらを連携させて仕事をしていることであろう。
 これらのソフトは,同時に実行されているように見えるが,実際には,非常に短い時間
でCPUが実行するソフトを切り替えているだけで,1個のCPUが実行しているコードは常時
1ヶ所しかないのである。
 しかし,見かけ上とはいえ,同時並行的にプログラムを動作させるのは,多くのメリッ
トを持っている。たとえば,ワープロソフトで印刷中にメールソフトではメールを受信す
る,といった効率的な仕事のすすめかたが可能になる。
 複数のプログラムを同時並行的に動作させることを,マルチプロセスとか,マルチタス
ク
と呼ぶ。一方,1つのプログラム内で,同時並行的に別々のコードを実行させることを,
マルチスレッド(multi thread)と呼び,同時並行的に実行されるそれぞれのコード単位を,
スレッド(thread)と呼ぶ。threadとは,英語で「糸」「織り糸」「議論の筋道」といった
意味がある。ここでのスレッドとは,「一連の実行コード」というニュアンスを持ってい
る。

●main()メソッドを実行するスレッド
 ところで,私たちは今までスレッドを意識的に使用したことは無かった。しかし,実際
はすでにスレッドを使用しているのである。通常,Javaのプログラムはクラスのmain()
メソッドを呼び出して実行している。実は,Javaのプログラムが起動されると,スレッド
を1個生成する。そしてこの「最初のスレッド」がmain()メソッドを呼び出す
のである。
なお,この「最初のスレッド」は,基本的には他のスレッドと同等のものである。

【スレッドの使用例】
 では,さっそくスレッドの使用例を紹介しよう。List 1は,一見何の変哲もないプログ
ラムである。main()メソッドを見よ。List 1-<①>〜<4>では,MyThread型のオブジェクト
を2個生成し,それぞれのstart()というメソッドを呼び出しているにすぎない。そして,
List 1-<5>では,100回繰り返すforループの中でランダムな回数だけ空ループをした後,
"World"と表示しているだけである。main()メソッドはこれで終了となっている。
List 1 MyThreadTest1.java

 これだけ見てみると,実行結果は容易に想像できよう。100回連続して画面に"World"
と表示されるだけのはずである。しかし,実際にList 1を実行すると。
  World
  World
  Hello
  World
  Java
  World
   … (以下略)
というように,100個の"World",100個の"Hello",100個の"Java"がいりみだれて表示
される。これは,スレッドによる並行処理が行われている証拠である。
 Javaでは,いろいろな資源(リソース, resource)や機能をクラスで記述・表現し,その
オブジェクトで管理する(例:ストリーム)。同様に,スレッドもクラスで記述・表現し,
オブジェクトで管理する
。
 List 1でスレッドを表しているのが,List1-<6>から始まるMyThreadクラスである。
MyThreadクラスは,Threadクラスのサブクラスになっている。このように,

  基本的にはスレッドはThreadのサブクラスとして定義する

のである。
 List 1-<7>はString型フィールドstrの定義で,List 1-<8>はコンストラクタの定義(文字
列を引数にとってstrフィールドにセットしている)である。これらは特別の意味は無い。問
題は次のList 1-<9>の部分である。
 List 1-<9>は,Threadクラスにあるrun()というメソッドをオーバライドしている。この
runメソッドのオーバライドがミソなのである。実は,Thread(およびそのサブクラス)型オブ
ジェクトのstart()メソッドを呼ぶと,run()メソッドが呼び出され,並行実行される
のである。
Fig.1-(1)にスレッドの基礎事項を示す。


 では,最初からList 1の動作を見ていこう。最初に,List 1-<①>と<2>で,それぞれ"Hello"
と"Java"という文字列をフィールドstrに格納した2つのMyThread型オブジェクトt1,t2を
生成している。
 List 1-<3>と<4>で,t1とt2のstart()メソッドを呼び出している。この呼び出しによって,
t1とt2のrun()メソッドの内容(List 1-<9>)が並行的に実行される。もちろん,「最初のスレ
ッド」が実行しているList 1-<5>も並行的に実行されているので,List 1の実行結果で"World",
"Hello","Java"がいりみだれて表示されたのである。
 Fig.2に,List 1で3つのスレッドが切り替わりつつ,並行に実行されている様子の例を
示す。まず,「最初のスレッド」から実行が始まり,t1スレッドを生成して,start()メソ
ッドを呼び出す(時間軸<①>)。そして,t1スレッドの実行が始まる(時間軸<2>)。次に,
ふたたび「最初のスレッド」に切りかわってt2スレッドの生成され,t2のstart()メソッド
を呼ぶ(時間軸<3>)。続いて,t2スレッドの実行が開始される(時間軸<4>)。以降,スレッ
ドが切り替わりながら,見かけ上,3つのスレッドが並行実行されていく。

 Threadのフィールドとメソッドの概要を参考に見てみたい人はこちら。
 なお,スレッド実行中には引数を渡せないので,スレッドで処理するデータは,List 1の
ように,コンストラクタで設定する
。
 余談だが,List 1-<5>やList 1-<9>の空ループ(何もしないループ)は,互いのスレッドが
切り替わるだけの十分な時間を与えるための,便宜的なものである。

【Runnnableインタフェイス】
 ところで,すでにスーパークラスを持っているクラスは,Threadクラスを継承できない。
これは,Javaでは直接のスーパークラスを1個しか持てないからである。こういった場合に,
Threadクラスを継承しなくても,スレッドクラスを作成する手段が用意されている。この
方法は,Runnableインタフェイス(Fig 3, Table 1)を実装し,run()メソッドをオーバライド
する(Fig.1-(2)(a))。
 そして,実装したクラスのオブジェクトをラップするようにしてThread型のスレッドオブ
ジェクトを生成する(Fig.1-(2)(b))。スレッドオブジェクトのstart()メソッドを呼んでスレ
ッドの実行を開始する(Fig.1-(2)(c))点は,Threadのサブクラスを作る方法と変わらない。


 Runnableインタフェイスを実装する方法で,List 1を書き換えたものをList 2に示す。
List 2-<6>以下で,List 2-<9>で定義しているAをスーパークラスとし,Runnableを実装し
ているMyRunnableクラスを定義して,runメソッドをオーバライドしている(List 2-<8>)。
 List 2-<①>と<2>では,MyRuunable型オブジェクトをラップするようにして,Thread型
オブジェクトを生成している。List 2-<3>と<4>で,start()メソッドを呼んで,スレッドを
開始している。
List 2 MyThreadTest2.java

【sleep()メソッドによる一時停止】
 List 2を見て気づいたかもしれないが,List 1-<5>やList 1-<9>では空ループだった部分
が,List 2-<5>とList 2-<8>では,Threadのクラスメソッドsleep()を呼ぶように変わって
いる。sleep()を呼んだスレッド(すなわり,現在実行中のスレッド)は,指定された時間だけ,
一時停止(スリープ)する。
 空ループは,つねに命令を実行しているためにCPUのパワーを無駄に消費している。しかし,
sleep()メソッドでスリープしている間は,そのスレッドはCPUのパワーを消費しない。よっ
て,一定時間の間,実行を中断する場合は,空ループではなく,sleep()を使うようにするべ
きである。

【スレッドやプログラムはいつ止まるか】
 スレッドは,そのスレッドのrun()メソッドが終了したところで終了する。現在実行中の
スレッドから,特定のスレッドを強制的に終了させることはできない。外部からスレッドを
終了させたい場合は,変数などを使って当該スレッドに終了するように伝え,スレッド自身
に終了してもらうべき
である。たとえば
  class A extends Thread {
   private boolean alive = true;
   void end() { alive = false; }
   public run() {
    while( alive ) { /* 処理 */}
    }
   }
  }
というように,スレッドを終了させるメソッド(この例ではend())を作成して,このメソッド
を呼ぶようにする。

【割り込み】
 しかし,sleep()や後述するwait()などのメソッドを実行してスレッドが停止状態にある
場合には,このような仕組みは使えない。そこで,停止状態にあるスレッドを強制的に再
開するために,「割り込み(interruption)」という仕組みを使う。
 sleep()や後述のwait()の呼び出しで停止状態にあるスレッドのinterrupt()メソッドを呼
び出すと,スレッドが再開されると同時に,sleep(),wait()からInterruptedException例外
が投げられる。List 2-<5>,<8>のsleep()の呼び出しで,InterruptedException例外をキ
ャッチしているのは,このためである。
 割り込まれたスレッドでは,
  try { sleep()やwait()の呼び出し }
  catch( InterruptedException ie ) {
   /* 割り込み時の処理 */
  }
というように,割り込み時の処理を記述できるので,ここでスレッドを終了するなどの判
断を行える。
 もし,sleep()やwait()を呼び出しておらず,停止中でないスレッドに対してinterrupt()
が呼ばれると,割り込まれたことを表す“割り込みステータス”がセットされる。割り込
みステータスを調べるには,クラスメソッドinterrupted()や,インスタンスメソッドの
isInterrupted()が使用できる(Table 1参照)。前者は,呼び出し後に割り込みステータスを
クリアするが,後者はクリアされないという違いがある。また,sleep(),wait()で停止中
のスレッドが割り込まれた場合は,割り込みステータスはクリアされる。

【同期】
 さて,次にマルチスレッド処理で重要な「同期(synchronization)」について解説しよう。
List 3とList 4は,ケーキ屋のシミュレーションである。List 3は,ケーキ屋を表すCakeShop
クラスで,店頭に置いてあるケーキの数を表すnumOfCakeフィールドと,それを初期化す
るコンストラクタ(List 3-<①>)が定義されている。List 3-<2>のgetNumOfCake()はケー
キの在庫数を返すアクセッサである。
List 3 CakeShop.java, List 4 ShopSim.java


 List 3-<3>から始まるsell()メソッドは,ケーキを1個売る,という処理を行う。List 3-<4>
でケーキの在庫があるかを確認したうえで,売れたらList 3-<6>でケーキ数を1個減らし,
trueを返す。在庫がなかった場合は,ケーキを売ることができないので,falseを返す。なお,
List 3-<4>は例によって便宜的なスリープである。
 List 4の前半は客を表すCustomerクラスの定義である。Customerはスレッドクラスになっ
ている。対象となるケーキ屋を参照するためのshopフィールドを設け,さらに客には識別番
号をつけたいので,最後に発行した識別番号を表すクラス変数id,客の識別番号myidの2つの
フィールドを用意している(List 4-<①>)。そして,コンストラクタでmyidとshopフィールド
をセットする。肝心のrun()メソッドでは,shop.sellCake()を呼び出してケーキ屋にケーキを
売ってもらった結果を表示する。
 List 4の後半は,シミュレーションを行うShopSimクラスが定義されている。List 4-<4>で,
10個のケーキの在庫がある状態でケーキ屋オブジェクトを生成し,List 4-<5>では,客スレ
ッドを15個生成・実行している。
 List 3とList 4を同じディレクトリに入れ,ShopSimクラスを実行すると,どうなるだろう
か。ふつうに考えれば,10個しかケーキが無いのだから,10人だけがケーキを買うのに成功
し,あとの5人はケーキを買えないはずである。しかし,実際にCakeSimを繰り返し実行して
みると,11人以上の客がケーキを買うことができる現象を確認できる。なぜ,このようなこ
とになったのだろうか。

●競合状態
 この問題が起こる様子(例)を,Fig.4に示す。Fig.4では,2つのCustomer型スレッドa,bが
実行される様子を示している。

今,残りのケーキ数(Fig.4の右)が1この状態で,aとbがsellCake()を呼び出すところから考え
てみよう。
 まず,スレッドaがsellCake()を呼び出す(Fig.4-<①>)。つぎにスレッドbに処理が切り替わ
り,スレッドbがsellCake()を呼び出す(Fig.4-<2>)。ふたたびスレッドaに処理が移り,ケー
キの在庫があるかどうかを確認する(Fig.4-<3>)。このとき,在庫数(numOfCakeの値)は1で
あるから,在庫があることになる。さらにスレッドbに切り替わり,同様に在庫数の確認が行
われるが,この時点でもケーキの在庫数は1で,在庫があることになってしまう。これが,
11人以上がケーキを買うことができた原因である。スレッドbは続いてケーキの在庫数を1個
減らし(Fig.4-<5>),ケーキの在庫数は0になる。次に,スレッドaもケーキの在庫数を減らし
(Fig.4-<6>),ケーキの在庫数は-1になってしまう。
 問題をまとめると,あるスレッドが「在庫数の確認→在庫数の変更」という処理を行ってい
る間に,他のスレッドが入り込んでしまうために,矛盾が生じている,というわけである。こ
のように,複数の並行処理がプログラムの整合性を破壊する状態を,競合状態(race condition)
と呼ぶ。
 このプログラムが処理が正常に動作するには,「在庫数の確認→在庫数の変更」という処理
の間に,在庫数を参照したり,変更する他のスレッドが実行されてはいけないのである。
 一般に,あるリソース(変数やオブジェクトなど)を「参照→変更」する間は,そのリソース
を参照したり変更する他の処理が並行で実行されてはならない
のである。一般に,あるリソー
スに対して,他のコードが並行実行されてはいけないコード区間をクリティカルセクション
(critical section)
と呼ぶ(Fig.4のスレッドaでは<3>から<6>の間)。
 競合状態を解消するためには,どのような仕組みがプログラミング言語に必要であろうか。
まず,あるリソースに対して互いに競合関係にあるコード区間を指定する機能が必要となる。
そして,指定されたコード区間をあるスレッドが実行している間は,他のスレッドは競合関係
にあるコード区間を実行できないようにする必要がある(言い換えれば,指定されたコード区間
を排他的に実行する,ということ。これを排他処理と呼ぶ)。Javaでは,同期コード(synchronized
code)
という考え方で,この仕組みを実現している。

●同期コード
 同期コード(synchronized code)の基本的な考え方をFig.5に示す。まず,あるリソースr
に関して互いに競合関係にあるコード区間AとBがある(Fig.5-(1))。これらのコード区間の
間で,「早い者勝ち」という紳士協定を結んでもらう。この協定を結んだコード区間を,
同期コードと呼ぶ。その協定の内容は,以下のようなものである。

 まず,これらのコード区間は,同じリソースをめぐって競合状態にあることを示すために,
共通のオブジェクトを目印として指定する。このオブジェクトは,リソースrと無関係でも
かまわない(Fig.5-(2))。そして,この目印用オブジェクトには,実行の優先権を表す
    「錠(ロック, Lock)」
を持っていてもらう(Fig.5-(3))。そのため,目印用オブジェクトを,「ロック用オブジェクト」
と呼ぶことにする。
 そして,同期コード部分が実行されると,その同期コードはまず,自動的に目印である
ロック用オブジェクトに「ロック」を要求する
(Fig.5-(1)<①>)。ロック用オブジェクトが
ロックを持っていれば,その同期コードはロックを獲得できる
(Fig.5-(1)<2>)。このロック
が同期コードの実行優先権を表しており,ロックを獲得できた同期コードは,実行を継続で
きる(つまり,同期コードとして指定された区間が実行されることになる)
。ロックを獲得した
同期コード部分が終了すると,その同期コードはロックを自動的に「解放」し,ロックはロ
ック用オブジェクトに戻される
。
 このロックを獲得できるのは「早い者勝ち」である。ある同期コードが実行を開始しよう
としてロックを要求しても,すでに他の同期コードにロックをとられている場合,そのスレッ
ドはそのロックが解放され,再取得が可能になるまで一時停止状態になる。ロックが解放さ
れると,Javaのシステムは,同じロックを待っている停止状態の同期コードの中からひとつ
を選んで,ロックを与え,実行を再開させるのである。
 このような仕組みで,同じロック用オブジェクトを指定している同期コードは,並行に実行
されることはなくなるというわけである。つまり,先に実行している同期コード部分が終了す
るのを待ってから,実行されるようになる。つまり,無秩序に並行実行されるのではなく,た
がいの実行を待つというタイミングの「同期をとって」実行されるようになる。これが同期コ
ードと呼ばれる理由である。
 なお,同期による排他処理は,同じロック用オブジェクトを使っている同期コードどうし
でないと働かないことに注意して欲しい
。同期コードと非同期コードは,並行に実行される
ことになる
。

●synchronized文
 では,実際に同期コードを指定する方法を紹介しよう(Fig.6)。同期コードの指定方法は
2つある。

まず,メソッドの一部を同期コードに指定するsynchronized文(Fig.6-(1))について見てみ
よう。同期コードの指定は,Fig.6-(1)のように,

  synchronized ( obj ) {
   同期コード部分
  }

というようにして同期コード部分を囲む。ここで,objは,ロック用オブジェクトである。繰
り返すが,ロック用オブジェクトは,競合関係の原因となっているリソースとは無関係なもの
でかまわない
。また,synchronized文で指定する同期コードは,インスタンスメソッドのコ
ードでもよいし,クラスメソッドのコードでもかまわない。実際にsynchronized文を使って,
List 3のCakeShopクラスを改良したものを,List 5に示す。List 5-<①>でロック用のオブジェ
クトobjを生成して,List 5-<2>とList 5-<3>では,それぞれobjをロック用オブジェクトに
指定して,競合するコード範囲を同期コードに指定している。
List 5 CakeShop.java, List 6 CakeShop.java

●synchronizedメソッド
 もうひとつの同期コードの指定方法は,同期メソッド(synchronized method)と言い,
ひとつのメソッド全体を同期コードに指定する方法で,単にメソッドの宣言の前に,予
約語synchronizedを書くだけである(fig.6-(2))。同期メソッドは,ロック用オブジェク
トを明示的に指定しない。
 実は,同期メソッドは自動的に指定されるのである。インスタンスメソッドを同期メソ
ッドにした場合は,そのメソッドの属するオブジェクト(this)がロック用オブジェクトに
指定される。クラスメソッドを同期メソッドにした場合は,そのクラスに対応したクラス
リテラル
(java.lang.Class型のオブジェクトで,クラスXのクラスリテラルはX.class)が,
ロック用オブジェクトとして指定される。
 なお,同期メソッドをオーバライドしたメソッドは,同期メソッドではなく普通のメソ
ッドになる。オーバライド版のメソッドを同期メソッドにしたい場合は,明示的に同期メ
ソッドとして定義する必要がある。
 synchronizedメソッドを使って,List 3のCakeShopクラスを改良したものを,List 6に
示す。List 6-<①>でgetNumOfCake()を,List 6-<2>でsellCake()をそれぞれ同期メソッ
ドとして定義している。

●同期の様子
 では,List 4とList 5の組み合わせ,もしくはList 4とList 6の組み合わせで,プログラ
ムを実行してみよ。今度は何度プログラムを実行しても,きっちりと10名の客だけがケー
キを買うことができ,5名の客はケーキを買えないことがわかる。
 List 4とList 5の組み合わせで実行した場合の,処理の流れの例をFig. 7に示す。Fig.7で
は,Customer型のスレッドaとbがある。スレッドaは,Fig.7-<①>の段階ですでにsellCake()
メソッドを呼び出しており(Fig.7-<0>),ロックはまだどの同期コードにも獲得されてい
ないとする。

 その後,スレッドbがsellCake()を呼び出す(Fig.7-<①>)。次にスレッドaが同期コード
部分に入り,ロックを要求する(Fig.7-<2>)。ロックはまだどの同期コードにも獲得され
てないので,メソッドaの同期コードはロックを獲得して,実行を継続する。続いて,ス
レッドaは,ケーキの在庫数を確認する(Fig.7-<3>)。
 そして,スレッドbに処理が移り,スレッドbの同期コードもロックを要求しようとする
(Fig.7-<4>)。しかし,ロックはすでにスレッドaの同期コードに獲得されてしまっている
ので,スレッドbの同期コードはロックの獲得に失敗して,実行を一時停止し,待機状態に
なる(Fig.7-<5>)。処理は,スレッドaにうつり,ケーキ数を1個減らす(Fig.7-<6>)。つい
で,スレッドaの同期コードは終了し,ロックを解放する(Fig.7-<7>)。
 ロックが解放されたので,Javaのシステムは,そのロックを獲得するのに失敗して待機
状態にあったスレッドの中からスレッドbを選んで,ロックを渡し,スレッドbの同期コー
ドを再開する(Fig.7-<8>)。その後,スレッドbの同期コードがFig.7-<9>,<10>と実行
され,同期コード部分が終わったところで,ロックが解放される(Fig.7-<11>)。同期コー
ドどうしが同期を取って,実行されている様子がわかるであろう。

●synchronnized文とsyncronizedメソッドの比較
 synchronnized文とsyncronizedメソッドには,それぞれ利点と欠点がある。synchronnized
文は,同期コードの範囲を最小限に設定することができることと,ロック用オブジェクト
を自由に選べる
ことが利点である。たとえば,List 3とList 4のケーキ屋シミュレーション
で,List 5とList 6は,List 3のケーキ屋クラスCakeShopクラスのメソッド定義を書き換え
て,同期コード導入していた。
 しかし,List 3のCakeShopが,コンパイル済みのクラスファイルで与えられていて,定
義が書き換えられない場合はどうするのだろうか。このようなとき,synchronized文を利
用すれば,List 4をList 7のように書き換えることで,競合を回避することができる。List 7
-<①>では,run()メソッドの中身を,shopで参照しているCakeShop型オブジェクトをロッ
ク用オブジェクトに指定する同期コードにしている。
List 7 ShopSim.java

 一方,synchronizedメソッドは,thisを自動的にロック用オブジェクトに設定してくれる。
これは,そのクラスのインスタンスメソッドに含まれるsynchronizedメソッドは,自動的
に互いに排他的な同期コードになる
,ということである(もちろん,thisをロック用オブジェ
クトに指定するsynchronized文も)。

●アクセッサの重要性
 一般に,あるクラスCのメンバであるリソースrをめぐって競合が起こる場合,rを参照し
たり変更するrのアクセッサを設定し,そのアクセッサを同期メソッドにすべきである
。
とくに,「rの参照後,rを変更する」というメソッドは同期メソッドにするといい。たとえ
ば,rの型をRとすれば,
  class C {
   private R r;
   synchronized R getR(){return r;}
   synchronized void setR( R r ) {
    this.r = r;
   }
   synchronized updateR() {
    rを参照して,rを変更する
   }
  }
という具合である。このように,リソースrを持つオブジェクトのメソッドが同期コードであ
る場合を,サーバサイド同期と呼ぶ。List 6のCakeshopクラスもこのような形になっている。
 もし,thisをロック用のオブジェクトに使いたくない場合や,適切なsynchronizedメソッド
が無い(もしくは定義できない)場合は,List 7のように,synchronized文を使って同期コード
を指定することになる
。このような場合をクライアントサイド同期と呼ぶ。

【デッドロック】
 同期は競合を防ぐ合理的で便利な仕組みであるが,使い方を誤るとスレッドを永遠に停
止させてしまう。たとえば,ロックを永遠に取得したままの同期コードがある場合,その
ロックの解放を待つ別の同期コードは,永遠に停止したままになってしまう。このような
状況をスターべーション(starvation,「餓死」という意味)と呼ぶ。
 スターべーションのもっとも発生しやすくもっとも深刻な状況が,デッドロック(dead
lock)
である。デッドロックは,複数のロックを取得する必要がある場合に発生する可能性
がある。デッドロックが起こると,複数のスレッドが永遠に停止したままになる。
 身近な例で考えてみよう。紙が一枚,鉛筆が一本有るとする。今,2人の人物・太郎君と
花子さんに「その鉛筆でその紙に,"こんにちは"と書け。」と同時に命令したとする。太郎
君が最初に紙を手にし,花子さんが鉛筆を手にした。するとどうだろう。太郎君には鉛筆が
足りないし,花子さんには紙が足りない。結局,2人とも相手が自分と必要としているもの
を手放すまで目的を達することができずに止まってしまうのである。

 List 8〜10に,もう少し具体的なデッドロックの発生例を示す。List 8とlist 9は,ある
銀行口座から別の銀行口座に送金する処理を行うプログラムである。list 8は口座クラス
Accountの定義である。List 8-<①>は,現在開設されている口座番号の最大値accountNumMax,
口座番号accountNum,残高balanceというフィールドと,それらの値を設定するコンス
トラクタである。List 8-<2>〜<4>は,同期アクセッサの定義である。
List 8 Account.java, List 9 TransferTransaction.java

 List 9は,送金トランザクションを行うスレッドクラスTransferTransactionの定義である。
(参考:トランザクション(transaction)とは,「取引」処理というような意味で,コンピュ
ータ業界では,要求に応えて行う一件一件の処理のことを表す) list 9-<①>は送金元口座source,
送金先口座dest,送金金額amountと,それらを設定するコンストラクタである。その後の
transfer()メソッドのList 9-<2>が送金を行う部分である。送金を行うので,送金元口座
オブジェクトと送金先口座オブジェクトを変更する必要がある(送金元の口座残高を減らし
て,送金先の口座残高をその分増やす)。そこで,送金元口座オブジェクトsourceと送金先
口座オブジェクトdestを

  synchronized( source ) {
   synchronized( dest ) {
    送金処理
   }
  }


というように,2重にロック用オブジェクトに指定し,その中で送金処理を行っている。List 9
-<3>で,run()メソッドからtransfer()メソッドを呼び出して,送金を実行するようになって
いる。
 List 10は,トランザクションを処理するシステムを表すTransactionSystemクラスの定義
である。List 10-<①>で送金元口座オブジェクトa1と,送金先口座オブジェクトa2を生成し
ている。そして,List 10-<2>では,a1からa2への送金トランザクションスレッドと,a2からa1
への送金トランザクションスレッドを生成して,それぞれ実行を開始させている。
List 10 TransactionSystem.java

 List 8〜10を実行すると,デッドロックが発生して2つのスレッドは停止してしまう(そのた
めプログラムも終了しない)。なぜであろうか。
 デッドロックが発生する様子を,Fig.8に示す。

a1からa2へ送金するスレッドをa,a2からa1へ送金するスレッドをbとする。スレッドa1が
transfer()を呼び出したとしよう(Fig.8-<①>)。ついでスレッドbもtransfer()を呼び出す(Fig.8-<2>)。
スレッドaに処理がうつり,スレッドaは同期コード部に入ってsource,つまりa1のロックを
取得する(Fig.8-<3>)。続いて,スレッドbも同期コード部に入って,source,つまりa2の
ロックを取得する(Fig.8-<4>)。
 つぎに,スレッドaに処理がうつって,dest,つまりa2のロックを取得しようとする(fig.8-<5>)。
ここからが問題で,a2のロックはすでにスレッドbの同期コードによって取得されているので,
スレッドaの同期コードはa2のロックの解放を待つために停止する
。処理がスレッドbにうつっ
て,dest,つまりa1のロックを取得しようとする(fig.8-<6>)。しかし,ここでもa1のロック
はすでにスレッドaの同期コードによって取得されているため,スレッドbの同期コードはa1の
ロックの解放を待つために停止する
。
 つまり,2つのスレッドの同期コードがお互いに,相手が保持しているロックの解放を待つた
めに,停止しているのだが,両方とも実行が停止しているので,ロックを解放することはない。
その結果,これら2つのスレッドは永遠に停止することになってしまうのである
。

●順序づけによるデッドロック回避
 デッドロックは,複数の同期コードで,複数のロック用オブジェクトからロックを取得
するとき,あべこべの順番でロックを取得するために発生するのである
。
 先ほどの,「太郎君と花子さんに1枚の紙と1本の鉛筆を渡して"こんにちは"と書け」と命令
する例で言うと,「太郎君が紙を最初に取り,次に太郎君が鉛筆が必要にもかかわらず,花子
さんが鉛筆をとってしまった」のが原因である。これを解決するには,命令に
  「必ず紙から手に取り,次に鉛筆を手に取ること」
と付け加えればよい。そうすれば,太郎君が最初に紙を手にした後で,花子さんは太郎君が紙を
手放すまで待つことになる。太郎君が紙の次に鉛筆を取って紙に"こんにちは"と書いて,紙と
鉛筆を手放すと,花子さんが紙・鉛筆の順で手にとって紙に"こんにちは"と書くことになり,
めでたく2人とも目的を達することが出来る。

 List 8〜10の例では,a1からa2へ送金する同期コードでは
  a1のロックを取得→a2のロックを所得
という順番でロックを取得したが,a2からa1へ送金する同期コードでは,
  a2のロックを取得→a1のロックを所得
という順番でロックを取得していた。これがデッドロックの原因である。もし,両方の同期
コードが
  a1のロックを取得→a2のロックを所得
という順番でロックを取得しようとすれば,デッドロックは起こらない。
 そこで,デッドロックを解決するには,ロック用オブジェクトに一意の識別番号をつけ,
その識別番号の小さい順からロックするようにすればいいことになる
。この方針に基づき,
実際にList 8〜10のデッドロックを回避してみよう。
 ロック用オブジェクトは,List 8で定義されているAccountのオブジェクトを使用してい
た(List 9-<2>)。さいわい,Account型オブジェクトには口座番号という一意の番号が割り
振られている。つまり,口座番号の小さい方からロックを取得するようにすればいいわけで
ある。List 9を改良したのが,List 11である。List 11-<①>で,口座番号が小さい口座オブ
ジェクトをfirstLockedに,口座番号が大きい方の口座オブジェクトをsecondLockedに設定
している。そして,List 11-<2>では,まずfirstLockedからロックを取得し,つぎにsecondLocked
からロックを取得するようになっている。
 List 8,List 9,Lis 11の組み合わせでプログラムを実行すると,デッドロックが発生せず,
正常にプログラムが動作することがわかる。
List 11 TransferTransaction.java


【「待機」と「通知」によるスレッド協調】
 行いたい処理によっては,スレッドを協調させて動作させる必要が出てくる。たとえば,
スレッドAがリソースを生産し,スレッドBがそのリソースを消費するような場合である。
このような関係を,一般に「プロデューサー(生産者)/コンシューマー(消費者)」関係と言
う。
 このようなケースで重要なのは,生産者スレッドがリソースを生産するまで,消費者スレ
ッドを「待機」させ,生産が終わったら,待機していた消費者スレッドにその旨を「通知」
して再開させる,という仕組みである。javaでは,Objectクラスに用意されているwait(),
notify(),notifyAll()というメソッドで,スレッドの「待機」と「通知」の仕組みを提供し
ている(Table 2)。

 まず,スレッドがロック用オブジェクトのwait()メソッドを呼ぶと,ロックが解放され,
そのスレッドは待機状態にる
。
 wait()によって待機状態に入ったスレッドを再開させるには,ロック用オブジェクトの
notify()メソッドかnotifyAll()によって「通知」を行う
。
 notify()メソッドは,そのロック用オブジェクトのwait()メソッドによって待機状態にな
っているメソッドのうち,どれかひとつだけに「通知」を行う
。nofity()を呼び出したスレ
ッドがロックを解放した後,「通知」されたスレッドは,そのロックを与えられて実行が
再開される。notify()メソッドは,「通知」するスレッドを指定できない。よって,待機し
ているスレッドがひとつだけであるときに使用すべきである
。
 notifyAll()メソッドは,そのロック用オブジェクトのwait()メソッドによって待機状態に
なっているすべてのメソッドに「通知」を行う
。nofityAll()を呼び出したスレッドがロック
を解放した後,「通知」された全スレッドのうちひとつが,そのロックを与えられて,実行
が再開される。他のスレッドはロックの解放を待つ状態になる。
 実際の使用例をList 12,List 13に示す。これは,ケーキ屋のシミュレーション(List 4,List 6)
を変更したもので,ケーキ屋の店頭でケーキが売り切れた場合は,ケーキを焼いて追加する
という処理を加えている。客は,「待機」と「通知」の仕組みを使って,ケーキが売り切れ
た場合は,ケーキが追加されるまで待って,ケーキが追加されたらケーキを買う,という行
動を取るようにする。
List 12 ShopSim.java, List 13 CakeShop.java



 では,実際にプログラムを見てみよう。List 12がList 4と違うのは,List 12-<①>で,
CakeShopクラスに新たに導入したaddANewCake()を呼んで,5個のケーキを追加している
ところだけである。主な「通知」と「待機」の仕組みは,List 13のCakeshopクラスの定義
に仕掛けられている。List 13-<①>があらたに加えられた同期メソッドaddANewCake()の
定義で,ケーキを加えた後,notifyAll()を呼んで「通知」を行っている。List 13-<2>では,

  while( getNumOfCake() <= 0 ) {
   try{ wait(); }
   catch(InterruptedException ie){}
  }

という形で,ケーキが1個以上になるまで「待機」を行っている。
 一般に,「待機」は,

  while( 待機条件 ) {
   try{ wait(); }
   catch(InterruptedException ie){}
  }
  待機後の処理

というループの形で行うべきである。なぜなら,同じロック用オブジェクトのwait()で待
機状態に入っているスレッドでも,待機の理由自体はスレッドごとに異なっている可能性
があるためである。つまり,「通知」されて再開したものの,実はそのスレッドの待機条
件はまだ真のままであることがあるわけだ。
 それだけでなく,「通知」を受け取った後から,待機条件が真になってしまうこともあ
り得る。たとえば,List 13の例では,ケーキが追加されてスレッドに「通知」された後で,
待機していたスレッドT1がケーキを買ってしまい,ふたたび0個になったところで,別の
待機していたスレッドT2が再開されるかもしれない。
 このことから,待機が解除されたら,まだ待機しつづけるべきか条件判定をし,待機しな
くてもいいときだけ,ループを抜けて,処理を行うようにする必要があると言える。
 List 12とlist 13を実行すると,客は全員,ケーキを買うことができることがわかる。

【まとめ】
 紹介できなかったスレッドに関する話題はまだあるが,割愛させていただく。参考書や
Java APIドキュメントを呼んで,勉強してみよ。

【次回は】
 次回は,GUIに関する開設をする予定である。



戻る