Search on the blog

2012年12月20日木曜日

Server-side Java(3) Servletのライフサイクル

サーブレットのライフサイクル
ざっくりまとめると、
  • コンテナ上にあるサーブレットのインスタンスは一つだけ
  • リクエスト毎にスレッドを作って処理を実行
  • インスタンスはコンテナが終了した場合(または一定時間アクセスされなかった場合)に破棄される
という感じ(だと理解している)[1]。 言葉だけ聞くとふーんって感じだけど、いろいろ遊べそうなのでごくごく簡単なコードを書いて実験してみた。やったことは、
  1. インスタンスが一つしかないことを確認
  2. スレッドセーフではないようなコードを実行してみる
  3. 2.をスレッドセーフにして動作確認
  4. デッドロックを発生させてみる

1. インスタンスが一つしかないことを確認
以下のサーブレットにアクセスすると、cntがカウントアップされていく。ブラウザを閉じたり、他のブラウザからアクセスしたりした場合もcntの値はどんどんカウントアップされていく。cntはクラス変数では無く、インスタンス変数なのに。。。ということで、インスタンスはコンテナ上に一つしかないのだなと確認できる。
package jp.blogspot.techtipshoge;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class threadTest1
 */
@WebServlet("/threadTest1")
public class threadTest1 extends HttpServlet {
    private static final long serialVersionUID = 1L;

    int cnt;

    @Override
    public void init() {
        cnt = 0;
    }

    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();

        out.println("<html>");
        out.println("<body>");

        out.println("cnt = " + cnt++);

        out.println("</body>");
        out.println("</html>");
    }
}


2. スレッドセーフではないようなコードを実行してみる
こんなサーブレットを書いて同時アクセスするとどうなるか試した。
package jp.blogspot.techtipshoge;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class threadTest2
 */
@WebServlet("/threadTest2")
public class threadTest2 extends HttpServlet {
    private static final long serialVersionUID = 1L;

    int cnt;

    @Override
    public void init() {
        cnt = 0;
    }

    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();

        out.println("<html>");
        out.println("<body>");

        out.println("cnt = " + cnt);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ++cnt;

        out.println("</body>");
        out.println("</html>");
    }

}
まず、普通に以下のページにアクセスする。表示するのに3秒くらいかかる。
次に2つのタブを開いてサーブレットに(ほぼ)同時アクセスしてみる。「一つ目のスレッドが更新する前に、二つ目のスレッドがcntの値を読み込むので、2つ目のスレッドの表示で2インクリメントされるべきところが1しかインクリメントされない」という状況を予想したが、そうならなかった。(ブラウザはGoogle Chrome 23.0を使用。)
確かスレッドはリクエスト単位じゃなくて、クライアント単位みたいな話をどこかで見たような。。と思ってググってるとそれらしいのを見つけた。
TomcatやJBossではApache httpdのMaxClientsと同じく、リクエスト単位ではなくクライアント単位(ソケット単位)でスレッドを割り当てる。このモデルでは、例えばmaxThreads="20"とかにしたら常に同時に20個のリクエストをさばいてくれる、という仮定は成り立たない。クライアントがkeep aliveで接続している間はスレッドも待ち続けるので、21番目のリクエストは先に接続した20のクライアントがkeep aliveを終了してコネクションを切断するまで処理されない。[2]
おそらく異なるタブでアクセスしても同一のクライアントとして扱われたのでスレッドが生成されなかったのだろう。

じゃ、ChromeとFirefoxでサーブレットに同時アクセスしてみるか。お、出来た。出来た。想定どおりのスレッドセーフではない現象が見られた。

3. 2.をスレッドセーフにして動作確認
以下のサーブレットにChromeとFirefoxを使って同時アクセスしてみた。先にcnt読み込み処理に入ったスレッドがこのサーブレットインスタンスをロックするので、他のサーブレットからはcntにアクセスできなくなってしまう。cntは期待どおりにインクリメントされた。2.のスレッドセーフじゃないバージョンは後から微妙に遅れてサーブレットにアクセスしたUAのレスポンスは3秒くらいだったが、3.のスレッドセーフバージョンの場合は6秒くらい待たされることになる。

package jp.blogspot.techtipshoge;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class threadTest3
 */
@WebServlet("/threadTest3")
public class threadTest3 extends HttpServlet {
    private static final long serialVersionUID = 1L;

    int cnt;

    @Override
    public void init() {
        cnt = 0;
    }

    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();

        out.println("<html>");
        out.println("<body>");
        
        synchronized (this) {
            out.println("cnt = " + cnt);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ++cnt;            
        }

        out.println("</body>");
        out.println("</html>");
    
    }

}


4. デッドロックを発生させてみる
とりあえずデッドロックしそうなコードを苦し紛れに書いてみた。奇数番目のアクセスはhogeから、偶数番目のアクセスはfugaからロックするというふうにしてみた。
ChromeとFirefoxで同時アクセスするとデッドロックした。
package jp.blogspot.techtipshoge;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class threadTest4
 */
@WebServlet("/threadTest4")
public class threadTest4 extends HttpServlet {
    private static final long serialVersionUID = 1L;

    private static class Clazz {
        
    }
    
    Clazz hoge;
    Clazz fuga;
    int flip;
    
    @Override
    public void init() {
        flip = 0;
        hoge = new Clazz();
        fuga = new Clazz();
    }

    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();

        out.println("<html>");
        out.println("<body>");
        
        ++flip;
        try {
            if (flip % 2 == 1) {
                synchronized (hoge) {
                    out.println("hoge locked.<br/>");
                    out.flush();
                    Thread.sleep(3000);
                    synchronized (fuga) {
                        out.println("fuga locked.<br/>");
                        out.flush();
                        Thread.sleep(3000);
                    }
                }
            } else {
                synchronized (fuga) {
                    out.println("fuga locked.<br/>");
                    out.flush();
                    Thread.sleep(3000);                    
                    synchronized (hoge) {
                        out.println("hoge locked.<br/>");
                        out.flush();
                        Thread.sleep(3000);
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        out.println("</body>");
        out.println("</html>");
    }

}

引用
[1] http://www.javadrive.jp/servlet/ini/index5.html
[2] http://d.hatena.ne.jp/nekop/20120424/1335254637

0 件のコメント:

コメントを投稿