オブジェクトのロックによる解決
|
|
オブジェクトのロックによる解決法
サーブレットがSingleThreadModelを実装していたり、distributableと宣言されて分散仮想ホストに配置されたりしているときは、共有資源は分離したサーブレットなどのオブジェクトとなる。各要求のスレッドがこのような共有オブジェクトをアクセスする場合は、オブジェクトのロックのメカニズムが使われる。
以下にPocketというクラスのプログラムを示す。このクラスがAnotherPocketMoneyというサーブレットが呼ばれた結果コンテナ上にインスタンス化されたときは、mother_sPocketMoney(母親の手元金)というオブジェクト名が付与される。このオブジェクトに子供たちが同時に手を突っ込めないようにdeposit(預け入れ)とdeduct(引きだし)のメソッドにはsynchronizedというキーワードが付され、同期化されている。一人の子供が引き出すのにもたもたしても、その間に他の子供がお金を引き出すことはなくなる。balance(残高照会)のメソッドは同時に複数の子供たちがアクセスしても良い。
Pocket(mother_sPocketMoney) |
package basic_servlets; /** *
PocketはPocketMoneyを管理する。オブジェクトのロックの実習用 * 作成日
: (01/07/10 16:08:31) *
@author: Terry */ public class Pocket{ private int balance;
//残高 private static Pocket instance;
//シングルトンのためのオブジェクト private Pocket(int initialBalance) {
//シングルトンのため、コンストラクタは公開しない balance = initialBalance; } public int balance() {
//残高照会 return balance; } public synchronized int deduct(int deduct)
{ //引き出し balance = balance - deduct; try{ //同期メソッド内での待機の実験用
Thread.sleep(5000);
//sleepは指定された時間は直列化される // wait(5000);
//waitは指定された時間は並列化される // wait((long)(Math.random() *
10000)); //waitであってもsleepであってもスレッドが // Thread.sleep((long)(Math.random()
* 10000));//ここで待機中時は他のスレッドはここまで来る }catch (InterruptedException ie){} return balance; } public synchronized int deposit(int deposit)
{ //入金 balance = balance + deposit; return balance; } public static Pocket getInstance() {
//自分自身のインスタンスを取得 if (instance == null){
//このインスタンスが存在しないときは生成 instance = new
Pocket(10000); //最初の有り金は10,000円とする } return instance; } } |
スレッド同士が同期化されたメソッドを呼ぶときには、次のようにオブジェクトがベースとなっている。
オブジェクト・ロックのルール |
- 同期化されたメソッドには複数のスレッドのアクセスが直列化されるが、その中でwaitによりスレッドが待機状態になったときは、次のスレッドはそこまでの進行が許される。(Thread.sleepの場合は後続のスレッドはそのメソッドの入り口で同期化される) - 同じオブジェクトの二つの同期化されたメソッド各々を、各々別のスレッドがアクセスしようとすると、二つのスレッド同士は同期化される。 - 同じオブジェクトのひとつの同期化されたメソッドと同期化されていないメソッドを各々別のスレッドがアクセスするときは、二つのスレッドは平行して進行する。 - 二つのスレッドが各々別のオブジェクトの同期化されたメソッドを呼ぶときは、二つのスレッドは平行して進行する。 - ただしスタテックなメソッド(クラス・メソッド)はオブジェクトではなくクラスとして上記ルールが適用される。つまり上記ルールのオブジェクトをクラスと置き換える。 |
Javaではとりわけ容易にこのオブジェクトのロックのメカニズムを利用できるようになっている。メソッドの定義に”synchronized”キーワードを付加すれば良いのである。あるスレッドがあるオブジェクトのsynchronizedメソッドを呼んだときは、該スレッドは該オブジェクトのロックを自動的に取得する。 他のスレッドが該オブジェクトの同じメソッド、あるいは該オブジェクトのほかのsynchronizedメソッドを呼んでも最初のスレッドがロックを保持している限り待たされ待ち行列に入れられる。最初のスレッドが該synchronizedメソッドを抜ければロックは解除される。これがさっきのルールの元の仕組みである。
ひとつ以上のsynchronizedメソッドを持つオブジェクトをモニタと称することにする。Javaではモニタとはスレッドを待機させ準次実行させることができるオブジェクトなのである。synchronizedメソッドを実行中のスレッドは、このモニタのオーナなのである。このことから、モニタはオブジェクトの機能であることが理解されよう。現に総てのクラスはスーパークラスとしてObjectというクラスを持ち、そこにwait()やnotify()などが定義されている。
それではこのmother_sPocketMoneyをアクセスするAnotherPocketMoneyサーブレットを示そう。このサーブレットはPocketMoneyサーブレットと類似しているので、説明は不要であろう。
AnotherPocketMoney |
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 AnotherPocketMoney extends javax.servlet.http.HttpServlet { Pocket
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
"AnotherPocketMoney, Version 1.0 by Terry"; } public
void init() { if (mother_sPocketMoney
== null){
mother_sPocketMoney = Pocket.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>AnotherPocketMoney");
out.println("</TITLE></HEAD><BODY><H1>AnotherPocketMoney</H1><PRE>");
//要求された引出し金の処理 if (session.isNew() ==
false)
{ //初めてではない、要求されたお金を出して残りをクライアントにかえす
int deduct = 0; //deductはローカル変数としなければならない
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() + "円です");
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) { } } } |
なお、このサーブレットはmother_sPocketMoneyからお金を引き出すだけで、誰もお金を追加する(deposit)ことがないので、そのうちポケットの中の有り金が空っぽになる。Mother_sPocketMoneyというオブジェクトは、コンテナに一旦ロードされると、管理者の介入または再ロードしないかぎり存続することに注意されたい。Pocketというクラスはただひとつのインスタンスしか存在しないようになっているいわゆる「シングルトン」と呼ばれるものである。その詳細は別途説明することにする。
synchronizedキーワードはメソッドだけにかけるものではない。コードのあるブロックを同期ブロックとして定義することも可能である。この場合もモニタとなるオブジェクトを明記しなければならない。これがさきほどの同期ブロックで対象オブジェクト指定が必要とした理由である。メソッド全体でなく、ブロックを同期ブロックとするのは、メソッド全体を同期化すると範囲が広すぎたり(同期メソッドでなくそのメソッド内の同期ブロックのほうが良い場合)、反対に同期メソッドだけでは範囲が狭すぎたり(同期メソッドを幾つか使っているブロックを同期化したほうが良い場合)して不都合が生ずる場合があるからである。
読者は、Pocketクラスのdeductメソッドのなかでのsleepやwaitを活かし、二つのブラウザ・ウィンドウからほぼ同時にAnotherPocketMoneyをアクセスして、その動作を確認されることをお勧めする。