具体的な問題(インスタンス変数)

 


 


具体的な問題(インスタンス変数)

 

ここでは共有される資源のスレッド間競合の問題を実際に確認するために、インスタンス変数での実験をおこなう。しかしこれは、インスタンス変数だけでなく、このサーブレット外の共有資源でも同じことが言える。データベースについては別に説明するにしても、その他の共有資源を持つアプリケーションは、これから皆さんは頻繁に開発することになる。

 

それではインスタンス変数を使う場合のスレッド安全の問題を実際に試してみよう。母親が手もと金(pocket)として1万円もっていたとする。クライアントである皆さんが子供たちの役を演じる。母親が今手もとにいくら残っているか子供たちに教えると、子供たちが母親のpocketから必要な金をとりだす。下図はこのサーブレットを二人の子供たちがほぼ同時にアクセスし、ひとりが550円、もうひとりが600円とりだそうとした例である。左の子供は550円をとりだそうとして正しくその金額がとりだせ、残金も正確に報告されている。ところが右側の子供は550円しか出せずしかも残金は8900円になっていてつじつまが合わなくなっている。

 


 


実は、この反対に自分が要求したよりも大きな(別のスレッドが要求した)金額がだせる危険性もある。これはdeductという変数をインスタンス変数としたために生じている。この変数はperformTaskメソッドの中のローカル変数にすべきもので、このメソッドの最初に定義すれば解決される。各自試して見られたい。

 

deductという変数をローカル変数にしたもまだpocketMoneyという変数が共有されている(というかもともと共有すべきものである)。そのために問題が生じる。その例を示そう。

 


 


上図の事例では、二人の子供がともに4000円の残金があるので2000円と2500円を取り出そうとしたものである。2000円をだそうとしたが500円の不足をきたしてしまった。この場合の問題は、二人の子供が同時に母親のポケットに手を突っ込んでしまったためで、一人しかポケットに手を突っ込んで中を調べ、取り出せないような仕掛け(ロックのメカニズム)が必要となる。

 

このサーブレットのプログラムを以下に示す。

PocketMoney

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 PocketMoney extends javax.servlet.http.HttpServlet {

 

    public static int pocketMoney = 10000;  //いまあるお金

    private int deduct;                     //ひきだすお金

   

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 "PocketMoney, Version 1.0 by Terry";

}

/**

 * マルチスレッドを配慮していないサーブレットの事例

 * pocketMoneyというインスタンス変数を使っている

 */

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>PocketMoney</TITLE>");

        out.println("<META HTTP-EQUIV=\"pragma\" CONTENT=\"no-cache\">");   //本ページはキャッシングをさせない

        out.println("<META HTTP-EQUIV=\"Expires\" CONTENT=\"-1\">");

        out.println("</HEAD><BODY><H1>PocketMoney</H1><PRE>");

        //要求された引出し金額の受理と処理

        if (session.isNew() == false)

        {   //初めてではない、要求されたお金を出して残りをクライアントにかえす

            deduct = 0;         //インスタンス変数を使うと問題が発生する!

//          int deduct = 0;     //deductはローカル変数としなければならない

            try{

                deduct = Integer.valueOf(request.getParameter("needMoney")).intValue();

                out.println(deduct + "円の要求");

        //      Thread.sleep((long)(Math.random() * 10000));    //10秒までのランダムな待ち時間をつくる

                }catch(NumberFormatException e){}

            if (pocketMoney >= deduct){

                synchronized(this){

                out.println(pocketMoney + "円の残金から" + deduct + "円を出します(残り" + (pocketMoney-deduct) + "円)");

                Thread.sleep((long)(Math.random() * 10000));    //10秒までのランダムな待ち時間をつくる

                pocketMoney = pocketMoney - deduct;

                out.println(deduct + "円を出しました(残り" + pocketMoney + "円)");

                }

            }else{

                out.println(pocketMoney + "円しか残っていません");

            }

        }  

        //応答のページの送信

        out.println("ただいまの残金は" + pocketMoney + "円です");  

        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)

    {

    }

}

}

 

このプログラムでは問題が発生しやすいようにThread.sleepメソッドを使って処理時間がランダムに生じるようにしてある。だからといって、これらの行を削除しても本質的な解決にはならず、ある確率で問題を内在していることになり、それは運用時点で必ず顕在化する。

 

 

前節     目次     次節