プログラミング応用b 第10回 『スレッド1』 |
【同期処理】
すでに説明したとおり,マルチスレッドを利用する際,競合が問題を起こすことが多い。
競合状態を解消するためには,どのような仕組みがプログラミング言語に必要であろうか。
まず,
・ あるリソース(変数など)に対して互いに競合関係にあるコード区間を指定する機能が必要となる。
そして,
・ 指定されたコード区間をあるスレッドが実行している間は,他のスレッドは競合関係にあるコード区間を実行できないようにする必要がある
言い換えれば,指定されたコード区間を排他的に実行する,ということ。これを排他処理とか同期処理と呼ぶ。
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 文で指定する同期コードは,非static メソッドのコードでもよいし, static メ
ソッドのコードでもかまわない。
実際に synchronized 文を使って,List 3の CakeShop クラスを改良したものを,List 5 に示す。
ロック用オブジェクトは,オブジェクトで有りさえすれば何でも構わないので, List 5-<①>では
Object型オブジェクト obj を宣言してロック用オブジェクトとしている。
List 5-<2>とList 5-<3>では,それぞれ obj をロック用オブジェクトに指定して,競合する
コード範囲(クリティカルセクション)を同期コードに指定している。
List 5 CakeShop.java, List 6 CakeShop.java
●synchronizedメソッド
もうひとつの同期コードの指定方法は,同期メソッド(synchronized method)と言い,
ひとつのメソッド全体を同期コードに指定する方法で,単にメソッドの宣言の前に,予約語
synchronized を書くだけである(Fig.6-(2))。同期メソッドは,ロック用オブジェクトを
明示的に指定しない。
実は,同期メソッドでは,ロック用オブジェクトは自動的に指定されるのである。
・非staticメソッド
を同期メソッドにした場合は,そのメソッドの属するオブジェクト自身(this)が自動的にロック用オブジェクトに指定される。
・クラスXの static メソッドを同期メソッドにした場合は,クラスXに対応したクラスリテラル X.class という特殊なオブジェクトが,自動的にロック用オブジェクトとして指定される。
※クラスリテラルという特殊なオブジェクトは,各クラスに1個ずつ自動的に用意されるオブジェクトのことで,詳しく知らなくても良い。
つまり,非staticな同期メソッドがちゃんと排他的に実行されるのは,同じオブジェクト
(=ロック用オブジェクト)に属する非staticメソッドどうしのみ,となるので注意しよう。
同様に,クラスXの static な同期メソッドがちゃんと排他的に実行されるのは,同じクラスX
(=対応するロック用オブジェクト X.class を持つ)に属するstaticメソッドどうしのみ,となる。
なお,同期メソッドをオーバライドしたメソッドは,同期メソッドではなく普通のメソッドになる。
オーバライド版のメソッドを同期メソッドにしたい場合は,明示的に同期メソッドとして定義
する必要がある。
同期メソッドを使って,List 3の CakeShop クラスを改良したものを,List 6 に示す。
List 6-①で getNumOfCake( ) を,List 6-②で sellCake( )をそれぞれ同期メソッド
として定義している。
●同期の様子
では,List 4 と List 5 の組み合わせ,もしくはList 4 と List 6 の組み合わせで,プログラムを
実行してみよ。今度は何度プログラムを実行しても,きっちりと10名の客だけがケーキを買う
ことができ,5名の客はケーキを買えないことがわかる(下図)。
List 4 と List 5 の組み合わせで実行した場合の,処理の流れの例を Fig. 7 に示す。
Fig.7 では,Customer型のスレッドaとスレッドbがあり,ケーキの在庫が2個の状態から
図示してある。スレッドa は,Fig.7-①の段階ですでに sellCake( ) メソッドを呼び
出しており(Fig.7-⓪),ロックはまだどの同期コードにも獲得されていないとする。
その後,スレッドb が sellCake( ) を呼び出す(Fig.7-①)。次にスレッドaが同期コード
部分に入り,ロックを要求する(Fig.7-②)。ロックはまだどの同期コードにも獲得され
てないので,メソッドa の同期コードはロックを獲得して,実行を継続する。続いて,スレッドa は,
ケーキの在庫数を確認する(Fig.7-③)。
そして,スレッドb に処理が移り,スレッドb の同期コードもロックを要求しようとする
(Fig.7-④)。しかし,ロックはすでにスレッドa の同期コードに獲得されてしまっている
ので,スレッドb の同期コードはロックの獲得に失敗して,実行を一時停止し,待機状態に
なる(Fig.7-⑤)。処理は,スレッドa にうつり,ケーキ数を1個減らす(Fig.7-⑥)。つい
で,スレッドa の同期コードは終了し,ロックを解放する(Fig.7-⑦)。
ロックが解放されたので,Java のシステムは,そのロックを獲得するのに失敗して待機状態
にあったスレッドの中からスレッドb を選んで,ロックを渡し,スレッドb の同期コードを
再開する(Fig.7-⑧)。その後,スレッドb の同期コードがFig.7-⑨,⑩と実行され,同期コード
部分が終わったところで,ロックが解放される(Fig.7-⑪)。
このように,
複数のスレッドの同期コードどうしが「ロックの解放と取得」のタイミングに合
わせて(=「同期をとって」)実行が切り替わる様子がお分かり頂けたと思う。
●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文およびその内部から呼び出されたsellCake()メソッド全体も同期コードとなる。
このとき,下図のように,CakeShop型オブジェクト shop が持つ唯一のロックをスレッド
実行される複数のクリティカルセクションが奪い合うので,同期処理がうまく働くのである。
一方,synchronizedメソッドは,thisを自動的にロック用オブジェクトに設定してくれる。
これは前述したように,そのクラスの非static な synchronized メソッドは,自動的に
互いに排他的な同期コードになる,ということである。
●よくあるsyncronizedメソッドに関する間違え
前項で,List 3 がコンパイル済みなどの理由でソースコードが変更できないとき, List 7 のように
クリティカルセクションを呼び出す側(Customerクラスの run( )メソッドで sellCake( ) メソッドを
呼び出す部分)自体を同期コードに指定することで対処する方法を学んだ。
ここで,よくある間違いは,『List 7 では synchronized 文を使って対処したけど,synchronized 文
で対処できるなら,synchronized メソッドでも対処できるはずだよな』と安易に思い込んで下図の
List 7-2 のように,Customer クラスの run( ) メソッドを synchronized メソッドにしてしまうケースである。
この List 3 と List 7-2 の組み合わせでは,やはり競合が起こってしまう。なぜそうなってしまうのだろうか。
List 7-2 ShopSim.java
List 7-2 では,Customer型オブジェクトの run( )メソッド自体をsynchronizedメソッドにしてしまった。
すでに紹介したように,synchronizedメソッドはそのメソッドが所属しているオブジェクトをロック用の
オブジェクトとして自動で指定したことになる。つまり,List 7-2では,ロック用オブジェクトとして,
各Customer型オブジェクト自身を指定したことになる。
これは,下図のように,スレッド実行される複数のクリティカルセクションが,自分専用のロック用オブジェクト
を持っていることになるので,『ロックの取得し放題』となってしまうので,同期(排他)処理がうまく働かない
のである。
このように,同期(排他)処理をうまくコーディングする時に重要なのは,synchronized文を使うにしても,
synchronizedメソッドを使うにしても,ただ一つのロック用オブジェクトが何かをしっかり意識すること
である。
●アクセッサの重要性
一般に,あるクラスCのメンバであるリソースrをめぐって競合が起こる場合,rを参照し
たり変更するrのアクセッサを設定し,そのアクセッサを同期メソッドにすべきである。
特に,「rの参照後,rを変更する」というメソッドは同期メソッドにするといい。たとえ
ば,rの型をRとすれば,
class C {
private R r;
synchronized R getR( ){ if( リソースrが有効か? ) return r; else null; }
synchronized void setR( R r ) {
this.r = r;
}
synchronized updateR( ) {
rを参照して,rを変更する
}
}
という具合である。
※ただし,
class C {
private R r;
synchronized R getR( ){ return r; }
}
の getR のように
「ただフィールドの値を返すだけ」というシンプルで標準的なゲッタに関
しては同期メソッドにしなくても支障は無い。
このように,リソースrを持つオブジェクトのメソッドが同期コードである場合を,サーバサイド
同期と呼ぶ。List 6のCakeshopクラスもこのような形になっている。
もし,thisをロック用のオブジェクトに使いたくない場合や,適切なsynchronizedメソッド
が無い(もしくは定義できない)場合は,List 7のように,synchronized文を使って同期コード
を指定することになる。このような場合をクライアントサイド同期と呼ぶ。
特定の機能を持つクラスを作成する場合,そのクラスまたはオブジェクトのメソッドが複数のスレッド
から呼ばれてもその機能がちゃんと動作するようにするには,アクセサを始め,その機能に関わるメソッド
をsynchronizedメソッドにする必要がある。
なお,特定の機能を持つ処理やオブジェクトなどが,同期処理などの対処を適切に施すことによって,
複数のスレッドから同時並行的に呼ばれても正常に動作することをスレッドセーフ(thread safe)と呼ぶ。
前回学習した Vector はスレッドセーフであり,ArrayListはスレッドセーフではない。同期コード
の実行には「ロックの獲得」など,余分な処理が入るために,同期コードではないコードを実行する
場合よりも若干時間が掛かる。VectorがArrayListよりも少し実行効率が悪いのはそのためである。
【次回は】
次回はスレッドの続きとして,同期が原因となる新たな問題「デッドロック」とその対策などを学ぶ。
【演習課題・問題2のヒント】
■講義資料「3. スレッドはいつ終了するか」の内容を理解していれば,スレッドを意図的に終了させるには,
各スレッドに特定の変数(フィールド)の内容をチェックさせて,その値によって自主的に終了させるようにすればいい,
ということは分かるはずです。
■この問題の場合,客番号6の客がチケットを購入した時点で,まだ購入していない複数の客スレッドを終了させる
必要があるわけです。個々の客スレッドはそれぞれCustomer型オブジェクトとして表されていますから,言い換え
ると,
「複数のCustomer型オブジェクトが共有している1つの変数の内容をチェックして終了するか判断する」
と言うことになりますね。
「同じ型の複数のオブジェクトが共有するただ1個の変数(フィールド)」
というのは,static フィールドのことですから,一番簡単なのは,
・Customerクラスの static な boolean型フィールド alive (初期値 true)を定義しておく。
※変数名も違って良いし,boolean型で無くても同じ目的が達成されていればかまわない
としておいて,
・Customerオブジェクト(スレッド)の run メソッドで,まず
alive の値を確認する。
・aliveの値が true なら shop.sellTicket メソッドを呼び出し1枚チケットを買おうとする。
買えたら
・購入結果のメッセージ文字列"〇"を作成。
・自オブジェクトの myId フィールドの値(客番号)が6番だったら,alive の値を false とする。
買えなかったら
・
購入結果のメッセージ文字列"×"を作成。
そして自分の客番号と購入の成否を表すメッセージ("〇"か"×")を表示。runメソッドを終了(=このメソッドが終了する)
・aliveの値が false なら,何もせずに run メソッドを終了する(=このメソッドが終了する)。
とすればいいはずです(下図)。
■しかし,
「複数のCustomer型スレッドが変数 alive の値を確認・変更する」
わけですから,新たな「変数 alive をめぐる競合」関係が生まれることになります。つまり,新たに同期処理を施さ
ないといけません。クリティカルセクションは
「変数aliveの値を確認して,何かしら処理を行った後でaliveの値を変更するまで」
となります。
同期処理を施すには目印となるオブジェクト(ロック用オブジェクト)が1個必要になるわけです。このプログラム
に1個だけ存在するオブジェクトとしては,既に TicketShop型の shop がありますが,shop はすでに
「複数のCustomer型スレッドが変数 numOfTicket の値を確認・変更する」
という「変数
numOfTicketをめぐる競合」のロック用オブジェクトとして使用されています。今回の講義で習った
同期処理の仕組みから言えば,TicketShop型オブジェクト shop に
・「変数
numOfTicketをめぐる競合」のロック用オブジェクト
・「変数 alive をめぐる競合」のロック用オブジェクト
の両方を兼ねさせることも可能ではあります。
しかし,それではオブジェクト shop が持つたったひとつのロックを争って
・「変数
numOfTicketをめぐる同期コード」実行中には,「変数 alive をめぐる同期コード」まで待ち状態になってしまう
・逆も然り
となって,効率が悪くなってしまいます。
やはり,「変数 alive をめぐる競合」の同期処理のためには,shop とは別に
専用のロック用オブジェクトを指定してやる方が良いでしょう。
以下の正解例では,Customerクラスの中で
static Object lockobj = new Object( ); // 本来は private にすべきだが,この課題ではそこまで求めない。
変数名や型名は違っていて良い。
として,1個の
Object型オブジェクトを生成してstatic フィールド lockobj で参照しておき,この lockobj を
「変数
alive をめぐる競合」を解決するための同期処理用ロックオブジェクトとして指定すればいいでしょう(下図) 。
以下に,実行結果をいくつか掲載する。いずれも
・6番の客が購入できなかった場合は問題1と同じ結果
・6番の客が購入できた場合は,6番目の客が最後の購入者で表示の最後になる
となっています。
■分からなかった人は以上の方針で作成してみましょう。
戻る