web-dev-qa-db-ja.com

HttpSession内での同期は可能ですか?

UPDATE:質問の直後のソリューション。

質問:

通常、同期とは、JVM内で並列リクエストをシリアル化することです。

_private static final Object LOCK = new Object();

public void doSomething() {
  ...
  synchronized(LOCK) {
    ...
  }
  ...
}
_

Webアプリケーションを見ると、「JVMグローバル」スコープでの同期がパフォーマンスのボトルネックになっている可能性があり、ユーザーの HttpSession のスコープ内でのみ同期する方が理にかなっています。

次のコードは可能ですか?セッションオブジェクトで同期することは良い考えだと思いますが、あなたの考えを聞くのは興味深いでしょう。

_HttpSession session = getHttpServletRequest().getSession();
synchronized (session) {
  ...
}
_

主な質問:
同じユーザーからのリクエストを処理するすべてのスレッドで、セッションオブジェクトが同じインスタンスであることが保証されていますか?

要約された回答/解決策:

セッションオブジェクト自体は、サーブレットコンテナの実装(Tomcat、Glassfishな​​ど)に依存するため、常に同じではないようで、getSession()メソッドはラッパーインスタンスのみを返す場合があります。

そのため、セッションに格納されているカスタム変数を使用して、ロックオブジェクトとして使用することをお勧めします。

これが私のコード提案です、フィードバックは大歓迎です:

ヘルパークラスのどこかに、例えばMyHelper

_private static final Object LOCK = new Object();

public static Object getSessionLock(HttpServletRequest request, String lockName) {
    if (lockName == null) lockName = "SESSION_LOCK";
    Object result = request.getSession().getAttribute(lockName);
    if (result == null) {
        // only if there is no session-lock object in the session we apply the global lock
        synchronized (LOCK) {
            // as it can be that another thread has updated the session-lock object in the meantime, we have to read it again from the session and create it only if it is not there yet!
            result = request.getSession().getAttribute(lockName);
            if (result == null) {
                result = new Object();
                request.getSession().setAttribute(lockName, result);
            }
        }
    }
    return result;
}
_

そして、あなたはそれを使うことができます:

_Object sessionLock = MyHelper.getSessionLock(getRequest(), null);
synchronized (sessionLock) {
  ...
}
_

このソリューションに関するコメントはありますか?

31
basZero

spring-mvc JavaDoc for WebUtils.getSessionMutex() にこの素晴らしい説明が見つかりました:

多くの場合、HttpSession参照自体も安全なmutexです。これは、同じアクティブな論理セッションの常に同じオブジェクト参照になるためです。ただし、これは、異なるサーブレットコンテナ間で保証されません; 100%安全な唯一の方法は、セッションミューテックスです。

このメソッドは、synchronizeOnSessionフラグが設定されている場合のロックとして使用されます。

_Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
    return handleRequestInternal(request, response);
}
_

getSessionMutex()の実装を見ると、実際には、存在する場合(_org.springframework.web.util.WebUtils.MUTEX_キーの下)にカスタムセッション属性が使用され、存在しない場合はHttpSessionインスタンスが使用されます。

_Object mutex = session.getAttribute(SESSION_MUTEX_ATTRIBUTE);
if (mutex == null) {
    mutex = session;
}
return mutex;
_

プレーンなサーブレット仕様に戻ります。100%にするために、HttpSessionオブジェクト自体ではなく、カスタムセッション属性を使用してください。

こちらもご覧ください

28

一般に、同じオブジェクトを返すHttpServletRequest.getSession()に依存しないでください。サーブレットフィルターが何らかの理由でセッションのラッパーを作成するのは簡単です。コードはこのラッパーのみを参照し、リクエストごとに異なるオブジェクトになります。セッション自体に共有ロックを設定します。 (残念ながらputIfAbsentはありません)。

8

同期は、オブジェクト参照がロックされると発生します。そのため、同じオブジェクトを参照するスレッドは、その共有オブジェクトの同期を料金所として扱います。

それで、あなたの質問は興味深い点を提起します:同じセッションからの2つの別々のWeb呼び出しのHttpSessionオブジェクトは、Webコンテナー内の同じオブジェクト参照として終了するのでしょうか、それとも、たまたま同じようなデータを持つ2つのオブジェクトですか?私は this HttpSessionについていくらか議論しているステートフルWebアプリに関する興味深い議論を見つけました。また、HttpSessionのスレッドセーフティについて このディスカッション がCodeRanchにあります。

それらの議論から、HttpSessionは確かに同じオブジェクトのようです。簡単なテストの1つは、単純なサーブレットを作成し、HttpServletRequest.getSession()を調べて、複数の呼び出しで同じセッションオブジェクトを参照しているかどうかを確認することです。もしそうなら、私はあなたの理論は正しいと思います、そしてあなたはそれを使ってユーザー呼び出しの間で同期することができます。

2
CodeChimp

すでに述べたように、セッションはサーブレットコンテナーによってラップされる可能性があり、これにより問題が発生します。セッションのhashCode()はリクエスト間で異なります。つまり、同じインスタンスではないため、同期できません。多くのコンテナはセッションの持続を許可します。この場合、特定の時間に、セッションが期限切れになると、セッションはディスクに保持されます。逆シリアル化によってセッションが取得された場合でも、シリアル化プロセスの前にメモリにあったときと同じメモリアドレスを共有しないため、以前と同じオブジェクトではありません。セッションがディスクからロードされると、「maxInactiveInterval」に到達する(期限切れになる)まで、セッションはメモリに格納されてさらにアクセスされます。要約すると、セッションは多くのWebリクエスト間で同じではない可能性があります。メモリ内でも同じです。属性をセッションに入れてロックを共有しても、永続化フェーズでシリアル化されるため、機能しません。

2

個人的に、私は HttpSessionListener *の助けを借りてセッションロックを実装します:

package com.example;

@WebListener
public final class SessionMutex implements HttpSessionListener {
    /**
     * HttpSession attribute name for the session mutex object.  The target for 
     * this attribute in an HttpSession should never be altered after creation!
     */
    private static final String SESSION_MUTEX = "com.example.SessionMutex.SESSION_MUTEX";

    public static Object getMutex(HttpSession session) {
        // NOTE:  We cannot create the mutex object if it is absent from  
        // the session in this method without locking on a global 
        // constant, as two concurrent calls to this method may then 
        // return two different objects!  
        //
        // To avoid having to lock on a global even just once, the mutex 
        // object is instead created when the session is created in the 
        // sessionCreated method, below.

        Object mutex = session.getAttribute(SESSION_MUTEX);

        // A paranoia check here to ensure we never return a non-null 
        // value.  Theoretically, SESSION_MUTEX should always be set, 
        // but some evil external code might unset it:
        if (mutex == null) {
            // sync on a constant to protect against concurrent calls to 
            // this method
            synchronized (SESSION_MUTEX) { 
                // mutex might have since been set in another thread 
                // whilst this one was waiting for sync on SESSION_MUTEX
                // so double-check it is still null:
                mutex = session.getAttribute(SESSION_MUTEX);
                if (mutex == null) {
                    mutex = new Object();
                    session.setAttribute(SESSION_MUTEX, mutex);
                }
            }
        }
        return mutex; 
    }

    @Override
    public void sessionCreated(HttpSessionEvent hse) {
        hse.getSession().setAttribute(SESSION_MUTEX, new Object());
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent hse) {
        // no-op
    }
}

セッションミューテックスが必要な場合は、次を使用できます。

synchronized (SessionMutex.getMutex(request.getSession())) {
    // ...
}

__

* FWIW、独立したリソースのリクエストが同じセッションロックを共有する必要がないように名前付きセッションロックを提供するため、質問自体で提案されたソリューションが本当に気に入っています。しかし、単一のセッションロックが必要な場合は、この答えはあなたのすぐ上にあるでしょう。

1
megaflop

正解です。同じユーザーが2つの異なる(または同じ)要求を同時に実行しないようにする場合は、HttpSessionで同期できます。これを行うには、フィルターを使用するのが最善です。

ノート:

  • リソース(画像、スクリプト、および非動的ファイル)もサーブレットを経由する場合、ボトルネックが発生する可能性があります。次に、同期は動的ページでのみ行われます。
  • GetSessionを直接避けてください。ゲスト用のセッションは自動的に作成されないため(セッションに何も格納する必要がないため)、セッションがすでに存在するかどうかをテストすることをお勧めします。次に、getSession()を呼び出すと、セッションが作成され、メモリが失われます。次にgetSession(false)を使用し、セッションがまだ存在しない場合はnullの結果を処理してみます(この場合は同期しないでください)。
1
William R

「MurachのJavaサーブレットとJSP(第3版)」ブックで提案されている別のソリューション:

Cart cart;
final Object lock = request.getSession().getId().intern();
synchronized (lock) {
    cart = (Cart) session.getAttribute("cart");
}
1
Max77

Tomasz Nurkiewiczによって言及されたSpringフレームワークソリューションは、サーブレット仕様が複数のJVM間でセッションの一貫性を必要とするため、クラスター環境で誤って正しくなります。それ以外の場合、複数のリクエストが異なるマシンに分散しているシナリオでは、それ自体が魔法をかけることはありません。 this thread の説明を参照してください。

0
Amin Mozafari

使用する

private static final Object LOCK = new Object();

すべてのセッションで同じロックを使用していて、それが私が直面したデッドロックの中心的な理由でした。そのため、実装のすべてのセッションで同じ競合状態が発生しますが、これは悪いことです。

変更が必要です。

その他の推奨される回答:

Object mutex = session.getAttribute(SESSION_MUTEX_ATTRIBUTE);
if (mutex == null) {
  mutex = session;
}
return mutex;

はるかに良いようです。

0
Mladen Adamovic