ビジーフラグによる解決
|
|
ビジーフラグによる解決
AnotherPocketMoneyではmother_sPocketMoneyオブジェクトのロックを使ってスレッド間の競合問題を解決しようとしている。残念ながら、これですべて問題が解決したわけではない。下図の事例をみてみよう。右側の子供に関しては全く問題がないが、左側の子供の場合は、残り9000円のはずであったが、1000円引き出した後で残りが7800円と表示されている。これは、左側の子供がdeductメソッドを出た途端に右側の子供がdeductに入り1200円引き出した為である。これは正しい結果ではあるが、クライアントにしてみれば奇異な印象を与える。最初の子供が残金を9000円と表示しおわるまで次の子供に一切mother_sPocketMoneyに手を出せないようにすればこの問題は解決される。
それではperformTask側でこの一連の操作をさっきのように同期ブロック化すればよいのかといえば、このサーブレットのインスタンスが多数存在する場合は各インスタンスからのスレッドが同じ状況をつくりだしてしまう。
このようにJavaが用意しているメカニズムだけではスレッド間の排他制御にこと足りるわけではない。その為にスレッド間にあるオブジェクトのアクセス可否を通知しあう仕掛けが必要である。これがビジー・フラグとかセマフォとかよばれるものである。
そのような事例はScott OrksとHenry Wongの”JAVA Threads”という著書(オライリージャパンから邦訳版が出版され、オーム社から発売されている(ISBN4-900900-54-0))の第3章に示されている。シナリオは銀行のATMで、「Bobは自分の口座の残高が$500あることを確認して$450引きだそうとしたが出来なかった。原因は全く同じ時刻にJaneが$100引き出したためである」。この事例はシステムとしては間違ってはいないが、顧客に対してはきちんとしたサービスにはなっていない。このとき顧客は不審に思って銀行に苦情を申し入れるであろう。従ってひとつの口座に対して同時にはひとつのATMにしかサービスできないとすべきであろう。銀行の窓口が係りをひとり置き(これはシングルトンのサーブレットが一括共有資源を管理する方法である)、これがATMと口座の対応をとり、複数のATMが同じ口座へのサービスを要求してきたらそれをひとつずつ対応すればよい。しかしながらそうしない場合は、モニタのメカニズムでこれを解決しようとしても無理である。ある顧客のセッションのあいだその顧客の口座のアクセスを他のスレッドにロックするのは、モニタのメカニズムの領域を越えている。
この著書の4.2節にBusyFlagというクラスが示されているので、参照されたい。ここではそのフラグを使ってAnotherPocketMoneyの問題を解決しよう。付属資料の本チュートリアルで使っているプログラム集にはBusyFlagのコードも含まれている。
まずこのBusyFlagクラスを組み込んだAnotherPocketというクラスを紹介しよう。
AnotherPocket |
package
basic_servlets; /** * AnotherPocketはPocketMoneyを管理する。オブジェクトのロックの実習用 * 作成日 : (01/07/10 16:08:31) * @author: Terry */ public
class AnotherPocket{ private int balance; //残高 private static
AnotherPocket instance; //シングルトンのためのオブジェクト private BusyFlag flag = new BusyFlag(); //ローカルなビジーフラグ(BusyFlag使用) private
AnotherPocket(int initialBalance) { //シングルトンのため、コンストラクタは公開しない balance = initialBalance; } public
int balance() {
//残高照会 return balance; } public
synchronized int deduct(int deduct) { //引き出し balance = balance -
deduct; try{
Thread.sleep((long)(Math.random() * 10000));//10秒までのランダムな待ち時間をつくる }catch
(InterruptedException ie){} return balance; } public
synchronized int deposit(int deposit) {
//入金 balance = balance +
deposit; return balance; } public
static AnotherPocket getInstance() { //自分自身のインスタンスを取得 if (instance ==
null){
//このインスタンスが存在しないときは生成
instance = new AnotherPocket(10000); //最初の有り金は10,000円とする } return instance; } public void lock(){
//ロックの為のメソッド
flag.getBusyFlag();
//BusyFlagの空くのを待ってそれを取得 } public void unlock(){
//ロック解除のためのメソッド
flag.freeBusyFlag();
//フラグを戻す } } |
このリストでは、いままでのPocketサーブレットに追加された個所を赤で示してある。このオブジェクトのロックのための二つのメソッドlockとunlockが追加された。これらのメソッドのなかで呼び出しているメソッドはBusyFlagクラスで定義されている。アプリケーションはこのオブジェクトをロックしたいときにlockメソッドを呼び、ロックを解除したいときにunlockを呼べばよい。このオブジェクトがあるスレッドによりロックされているときには、他のスレッドがlockを読んでも待機させられる。このオブジェクトを使ったサーブレットのリストは次のようになる。
AnotherPocketMoneyAgain |
package
basic_servlets; import
java.io.*; import
java.lang.*; import
java.util.*; import
javax.servlet.*; import
javax.servlet.http.*; /** * オブジェクトのロックの実習サーブレット * 作成日 : (01/07/06 13:35:24) * @author: Terry */ public
class AnotherPocketMoneyAgain extends
javax.servlet.http.HttpServlet { AnotherPocket mother_sPocketMoney; public void doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, java.io.IOException { performTask(request,
response); } public
void doPost(javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response) throws
javax.servlet.ServletException, java.io.IOException { performTask(request,
response); } public
String getServletInfo() { return
"AnotherPocketMoneyAgain, Version
1.0 by Terry"; } public
void init() { if (mother_sPocketMoney
== null){
mother_sPocketMoney = AnotherPocket.getInstance(); } } /** * オブジェクトのロックを使ったサーブレットの事例 * mother_sPocketMoneyというオブジェクトを使っている */ public
void performTask(javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response) { try {
//到来要求が最初のものかどうか調べる
HttpSession session = request.getSession();
//応答ページ作成の準備
response.setContentType("text/html; charset=Shift_JIS");
//Tomcatの場合は、このヘッダ行を追加することを推奨 //
response.setHeader("Cache-Control", "no-cache=\"set-cookie,set-cookie2\"");
PrintWriter out = response.getWriter();
out.println("<HTML><HEAD><TITLE>AnotherPocketMoneyAgain");
out.println("</TITLE></HEAD><BODY><H1>AnotherPocketMoneyAgain</H1><PRE>");
//要求された引出し金の処理 if (session.isNew() ==
false)
{
//初めてではない、要求されたお金を出して残りをクライアントにかえす
int deduct = 0;
//deductはローカル変数としなければならない
mother_sPocketMoney.lock(); //ここでmother_sPocketMoneyをロックして他のスレッドを待たせる
try{
deduct =
Integer.valueOf(request.getParameter("needMoney")).intValue();
out.println(deduct + "円の要求");
}catch(NumberFormatException e){}
if (mother_sPocketMoney.balance() >= deduct){
out.println(mother_sPocketMoney.balance() + "円の残金から" +
deduct + "円を出します(残り" + (mother_sPocketMoney.balance()-deduct) +
"円)");
mother_sPocketMoney.deduct(deduct);
out.println(deduct + "円を出しました(残り" +
mother_sPocketMoney.balance() + "円)");
}else{
out.println(mother_sPocketMoney.balance() + "円しか残っていません");
} }
//応答のページの送信 // Thread.sleep((long)(Math.random()
* 10000)); //10秒までのランダムな待ち時間をつくる
out.println("ただいまの残金は" + mother_sPocketMoney.balance() + "円です"); mother_sPocketMoney.unlock();
//ここでmother_sPocketMoneyのロックを解除
out.println("</PRE>");
String url = "http://" + request.getServerName() +
":" +
request.getServerPort() + request.getRequestURI();
String url1 = response.encodeURL(url); //このメソッドはWTEでは機能しないので現在使っていない
if (request.isRequestedSessionIdFromCookie()==false){ //Tomcatの場合はここをコメントアウトする
url1 = url + ";jsessionid=" + session.getId(); }
out.println("<FORM ACTION=\""+url1+"\"
METHOD=\"POST\">");
out.println("<BR>欲しいお金は:<INPUT TYPE=\"text\"
NAME=\"needMoney\">");
out.println("<INPUT TYPE=\"submit\" VALUE=\"おねだり\"></FORM></BODY></HTML>");
//クライアントに送信
out.flush();
out.close();
response.flushBuffer(); } catch(Throwable
theException) { } } } |
今までのAnotherPocketMoneyに追加された個所を、やはり赤で示してある。この例ではmother_sPocketMoneyオブジェクトにアクセス開始直前にlockをかけ、最後にアクセスした直後にunlockをかけている。初回の場合はlockをかけないでunlockすることになるが、問題は無い。ただロックする範囲を広くすればするほどスループットが下がることに注意しなければならない。
クライアント画面の表示は顧客に不自然な印象をもたせなくなっていることをデスクトップ上に二つのブラウザを走らせて確認してみよう。また、これでこのサーブレットがSingleThreadModelを実装して複数のインスタンスになっても問題の生じないスレッド安全なものとなっていることも確認しよう。