- PR -

ServletのdoGet, doPostはsynchronizedが必須!?

投稿者投稿内容
Hiro2004
会議室デビュー日: 2004/12/17
投稿数: 4
投稿日時: 2004-12-17 10:27
サーブレット内で以下の処理でサーブレットでセッション変数の一覧を取得しようとした所、
java.util.ConcurrentModificationExceptionが発生しました。


public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

.
.
.
HttpSession session = request.getSession();
Enumeration names = session.getAttributeNames();
while(names.hasMoreElements()) {
Object o = names.nextElement(); // <-------- ここでConcurrentModificationExceptionが発生した。
if (o instanceof String) {
System.out.println((String)o);
}
}
.
.
.
}



これは、たまたまこれと同じタイミングで同じセッションに対してセッション変数の追加、または削除が行なわれたために発生したものでしょう。

そこで疑問が出てきました。
ConcurrentModificationExceptionを避けるためには、上記のsessionを使ってsynchronizedをすればいいでしょう。
しかし、セッション変数の一覧を取得する処理は多数あります。
また、sessionに対するsetAttribute, removeAttributeを実行する処理も同じく多数あります。

そうすると、synchronizedを使う場所が多数でてきます。
セッション変数の操作でgetAttribute以外の処理を行なう場所はほぼ全てsynchronizedをつける必要があります。

synchronizedを多用するとそれだけパフォーマンスが落ちると推測しています。
ならば、どうせならばdoPost全体にsynchronizedを設定したほうが早いように思われます。

多重フレームで動かせば同一セッションへのアクセスは発生するでしょうし、小刻みなクリックによる同一セッション内での
サーブレットの二重起動もあるでしょうから、同一セッションへの多重アクセスは避けれないと思われます。

となると、

「セッション変数の一覧の取得を行なうサーブレットは必ずdoPostの内部全体をsessionによるsynchronizedを行なう必要が出てくる。
さもないと、場合によってはConcurrentModificationExceptionが発生する可能性がある」

と、なります。
これは正しいでしょうか?
それとも、ConcurrentModificationExceptionを発生させないでセッション変数一覧を取得する方法があるのでしょうか?

これが正しいとすると、何故、多重アクセスでエラーとなるようなAPIがHttpSessionに存在するのか不思議になってきます。
Edosson
ぬし
会議室デビュー日: 2004/04/30
投稿数: 675
投稿日時: 2004-12-17 11:18
こんなかんじでいいのでは。
コード:
while(names.hasMoreElements()) {
  try {
    Object o = names.nextElement();
    if (o instanceof String) {
      System.out.println((String)o);
    }
  } catch (ConcurrentModificationException e) {
    // 何もしない
  }
}


問題は、Sessionやその属性に対する多重アクセスじゃないと思います。

Enumeratorに格納された参照先が消滅した、ということですよね。
逆に、Enumeratorを取得した後、Sessionに、新たな属性が追加される場合もあり、
その場合は、すでに取得したEnumeratorからは参照できないことですし。
かつのり
ぬし
会議室デビュー日: 2004/03/18
投稿数: 2015
お住まい・勤務地: 札幌
投稿日時: 2004-12-17 11:27
引用:

これが正しいとすると、何故、多重アクセスでエラーとなるようなAPIがHttpSessionに存在するのか不思議になってきます。



HttpSessionはインターフェイスですので、
多重アクセスでエラーになるかは実装次第です。

HttpSessionを実装したクラスが返した、
Enumerationを実装したクラスが
たまたまConcurrentModificationException
を投げる作りになっていたと言うことです。

これはすべてのServletに当てはまりません。
取りあえず対処するならEdossonさんの方法でよいかと思います。
takamaro
大ベテラン
会議室デビュー日: 2004/10/12
投稿数: 100
投稿日時: 2004-12-17 12:32
引用:

ConcurrentModificationException は、バグを検出するためにのみ使用すべきです


とドキュメントに明記されている限りは、try-catchで拾うのは避けるべきかと思われます。
そもそもdoPostメソッドで行なわれている処理が、並行処理を「是」としているのかどうか
そのへんが重要になるのではないでしょうか。
「是」とするならば、今回のようにConcurrentModificationExceptionが発生しうる箇所
のみをsynchronizedブロックで囲めば良いでしょう(発生箇所が多く、synchronizedによる
オーバーヘッドが並行処理のメリットを上回るのならば、この限りではありませんが)
「否」とするならば、doPost全体をsynchronizedにするか、または他の方法でメソッドへの
進入を拒むべきかな、と思います。

引用:

これが正しいとすると、何故、多重アクセスでエラーとなるようなAPIがHttpSessionに存在するのか不思議になってきます。


それを言っちゃうと「何でHashMapなんてスレッドセイフじゃないコレクションをSunは新たに
用意したんだ?」って事にもなっちゃいます。
時として「必要な事」が、或る時には「余計な事」になっちゃったりするもんです。
Hiro2004
会議室デビュー日: 2004/12/17
投稿数: 4
投稿日時: 2004-12-17 13:59
返答ありがとうございます。

>>Edossonさん、かつのりさん
なるほど、エラーが出ても無視すればいいわけですね。
ただ、削除されたセッション変数名だけがなくなればいいのですが、削除されたもの以外のセッション変数名まで取得できなくなる可能性はあるような気がします。

発生する可能性が少ないので、全体に無限ループをかけて、

コード:

HttpSession session = request.getSession(); 
while(true){ // 一番外側の無限ループ

  Enumeration names = session.getAttributeNames(); 
  boolean errFlg = false;
  while(names.hasMoreElements()) {
    try {
      Object o = names.nextElement();
      if (o instanceof String) {
        System.out.println((String)o);
      }
    } catch (ConcurrentModificationException e) {
      errFlg = true; // エラー発生
    }
  }

  if (errFlg) {
    System.out.println("***エラーが発生したので再実行します***"); // エラーが発生したのでやりなおし
  } else {
    break; // エラーが発生しなかったので無限ループを抜ける
  }

} // 一番外側の無限ループ終了




のように成功するまでループさせようかとも考えたのですが、長期間ループを抜けない、またはいつまでたってもループを抜けなくなる可能性を考えて躊躇しています。



>>takamaroさん

引用:

takamaroさんの書き込み (2004-12-17 12:32) より:
「是」とするならば、今回のようにConcurrentModificationExceptionが発生しうる箇所
のみをsynchronizedブロックで囲めば良いでしょう(発生箇所が多く、synchronizedによる
オーバーヘッドが並行処理のメリットを上回るのならば、この限りではありませんが)



ConcurrentModificationExceptionが発生しうる箇所だけならばいいのですが、このエラーを引き起こすHttpSessionのsetAttribute, remoteAttributeメソッドにも同じようにsynchronizedブロックをかける必要がありますよね?
セッション変数の操作はサーブレットの基本動作ですから、かなり多数になると思います。

コーディング量も気になるのですが、頻繁にsynchronizedをするためにパフォーマンスが落ちるような気がしています。

引用:

「否」とするならば、doPost全体をsynchronizedにするか、または他の方法でメソッドへの
進入を拒むべきかな、と思います。



うーん、やはり、そうなりますよね。



引用:

引用:

これが正しいとすると、何故、多重アクセスでエラーとなるようなAPIがHttpSessionに存在するのか不思議になってきます。


それを言っちゃうと「何でHashMapなんてスレッドセイフじゃないコレクションをSunは新たに
用意したんだ?」って事にもなっちゃいます。
時として「必要な事」が、或る時には「余計な事」になっちゃったりするもんです。



なるほど。synchronizedを自分でつけることは可能だけれど、はずすとなると別クラスを用意する必要がでてきますね。
Edosson
ぬし
会議室デビュー日: 2004/04/30
投稿数: 675
投稿日時: 2004-12-17 14:25
引用:

Hiro2004さんの書き込み (2004-12-17 13:59) より:

ただ、削除されたセッション変数名だけがなくなればいいのですが、削除されたもの以外のセッション変数名まで取得できなくなる可能性はあるような気がします。


「削除されたもの以外」で、かつ取得できないセッション変数って具体的にどんなものですか?
内部でシステムがどんな使い方をしているのか知れたものではないとなれば、
それが本当に対処しなければならないものなのかどうか、の見極めも必要と思いますが。
かつのり
ぬし
会議室デビュー日: 2004/03/18
投稿数: 2015
お住まい・勤務地: 札幌
投稿日時: 2004-12-17 14:30
通常セッションは各ユーザ単位で発行される物であって、
同一セッションに対して非同期で動くことはあまりありえないケースですよね。
(IEでCtrl+Nを押して同じ画面を2つ開く等・・・)

ならばHttpServletのserviceメソッドをオーバーライドして

コード:
public void service(HttpServletRequest req,HttpServletResponse res) throws 省略
    synchronized(req.getSession(true)){
        super.service(req,res);
    }
}


みたいに記述すれば、同一セッションでのみスレッドセーフになります。
この記述をしたサーブレットをスーパークラスとするか、
コントローラとして使用すれば
ある程度安全に運用は可能じゃないかなと思います。
同期コストは必要ですが、マルチスレッドで動くので
パフォーマンスも悪くないはずです。

(ただし一部のサーブレットコンテナでは
getSession().toString().intern()で同期を行わなければいけませんが。)
Hiro2004
会議室デビュー日: 2004/12/17
投稿数: 4
投稿日時: 2004-12-17 15:03
引用:

Edossonさんの書き込み (2004-12-17 14:25) より:
引用:

Hiro2004さんの書き込み (2004-12-17 13:59) より:

ただ、削除されたセッション変数名だけがなくなればいいのですが、削除されたもの以外のセッション変数名まで取得できなくなる可能性はあるような気がします。


「削除されたもの以外」で、かつ取得できないセッション変数って具体的にどんなものですか?
内部でシステムがどんな使い方をしているのか知れたものではないとなれば、
それが本当に対処しなければならないものなのかどうか、の見極めも必要と思いますが。



セッション変数の値ではなく、一覧から取得するセッション変数名です。
前述のコードでも、セッション変数の値は全く考慮していなくて、セッション変数名だけを処理することを考えています。

例えば、セッション変数の名前として"V1", "V2", "V3", "V4", "V5"があったとします(値はここでは触れません)。
「Enumeration names = session.getAttributeNames(); 」を実行してセッション変数名(セッション変数の値ではない)の一覧を取得した後
このEnumeration namesからセッション変数名をnextElement()で順次取り出している時、
"V1"の名前を取り出した直後に別のスレッドで「session.removeAttribute("V3")」が行なわれた場合、どうなるか、を考えています。

"V3"まで読み進んでからエラーになる?
それとも、Enumeration names自体が無効になり、次の"V2"の名前を取得する時にエラーになる?
"V3"はエラーになるとして、次の"V4"はエラーにならない?

最悪の場合、"V2"の名前の取得でエラーになることも考えなければいけないし。
"V4"以降でもエラーが発生することも考えなければならないかな、と思っています。

スキルアップ/キャリアアップ(JOB@IT)