Возможно ли синхронизация внутри HttpSession?

UPDATE: Решение сразу после вопроса.

Вопрос:

Обычно синхронизация выполняет сериализацию параллельных запросов в JVM, например.

private static final Object LOCK = new Object();

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

При взгляде на веб-приложения некоторая синхронизация в области "JVM global" может стать узким местом и синхронизацией производительности только в рамках пользователя 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) {
  ...
}

Любые комментарии к этому решению?

Ответ 1

Я нашел это приятное объяснение в JavaDoc для WebUtils.getSessionMutex():

Во многих случаях ссылка HttpSession - это безопасный мьютекс, так как он всегда будет той же ссылкой на объект для одного и того же активного логического сеанса. Однако это не гарантируется в разных контейнерах сервлетов; единственным 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.

См. также

Ответ 2

В общем, не полагайтесь на HttpServletRequest.getSession(), возвращающий тот же объект. Легко для сервлет-фильтров создавать оболочку вокруг сеанса по любой причине. Ваш код будет видеть только эту оболочку, и это будет другой объект для каждого запроса. Поместите некоторую общую блокировку в сам сеанс. (Слишком плохо, но нет putIfAbsent).

Ответ 3

Синхронизация происходит, когда блокировка помещается в ссылку на объект, так что потоки, которые ссылаются на один и тот же объект, будут обрабатывать любую синхронизацию на этом общем объекте в качестве платного шлюза.

Итак, что ваш вопрос вызывает интересный момент: ли объект HttpSession в двух отдельных веб-вызовах из одного и того же сеанса заканчивается как одна и та же ссылка на объект в веб-контейнере, или они представляют собой два объекта, которые просто имеют похожие данные в них? Я нашел эту интересную дискуссию о веб-приложениях с поддержкой состояния, которые несколько обсуждают HttpSession. Кроме того, есть это обсуждение в CodeRanch о безопасности потоков в HttpSession.

Из этих обсуждений кажется, что HttpSession действительно является одним и тем же объектом. Один простой тест - написать простой сервлет, посмотреть HttpServletRequest.getSession() и посмотреть, ссылается ли он на тот же объект сеанса на несколько вызовов. Если да, то я думаю, что ваша теория звучит, и вы можете использовать ее для синхронизации между пользовательскими вызовами.

Ответ 4

Как уже говорили люди, сеансы могут быть обернуты контейнерами сервлетов, и это порождает проблему: hashCode() сеанса отличается от запросов, т.е. они не являются одним и тем же экземпляром и поэтому не могут быть синхронизированы! Многие контейнеры позволяют продолжать сеанс. В этом случае в определенное время, когда сеанс истек, он сохраняется на диске. Даже когда сеанс извлекается путем десериализации, он не является тем же объектом, что и раньше, поскольку он не имеет одинакового адреса памяти, например, когда он был в памяти до процесса сериализации. Когда сеанс загружается с диска, он помещается в память для дальнейшего доступа, пока не будет достигнут "maxInactiveInterval" (истекает). Подведение итогов: сессия может быть не одинаковой между многими веб-запросами! Это будет то же самое, что и в памяти. Даже если вы поместите атрибут в сеанс для совместного использования блокировки, он не будет работать, потому что он также будет сериализован в фазе сохранения.

Ответ 5

Ответы верны. Если вы хотите, чтобы один и тот же пользователь выполнял одновременно два разных (или одинаковых) запроса, вы можете синхронизировать их с HttpSession. Лучше всего это использовать фильтр.

Примечания:

  • Если ваши ресурсы (изображения, скрипты и любой нединамический файл) также попадают через сервлет, вы можете создать узкое место. Затем убедитесь, что синхронизация выполняется только на динамических страницах.
  • Старайтесь избегать getSession напрямую, вам лучше проверить, существует ли сеанс, потому что сеанс не создается автоматически для гостей (так как в сеансе ничего не нужно хранить). Затем, если вы вызываете getSession(), сеанс будет создан, и память будет потеряна. Затем используйте getSession(false) и попытайтесь обработать результат null, если сеанс уже не существует (в этом случае не синхронизируйтесь).

Ответ 6

Другое решение, предлагаемое в книге "Servlets Java Servlets и JSP (3rd Edition)":

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

Ответ 7

Лично я реализую сеансовую блокировку с помощью 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, мне очень нравится решение, предложенное в самом вопросе, поскольку оно предусматривает именованные блокировки сеанса, так что запросы на независимые ресурсы не должны использовать одну и ту же сессионную блокировку. Но если один сеанс блокировки - это то, что вы хотите, тогда этот ответ может быть прямо на вашей улице.

Ответ 8

Рамочное решение spring, упомянутое Томашем Нуркевичем, случайно в кластеризованных средах, только потому, что спецификация Servlet требует согласованности сеанса на нескольких JVM. В противном случае он не делает магии самостоятельно для сценариев, где несколько запросов распространяются на разных машинах. См. Обсуждение в этой теме, которое проливает свет на объект.

Ответ 9

Используя

private static final Object LOCK = new Object();

вы используете один и тот же замок для всех сеансов, и это была основная причина тупика, с которым я столкнулся. Поэтому каждый сеанс в вашей реализации имеет одно и то же состояние гонки, что плохо.

Он нуждается в изменении.

Другой предложенный ответ:

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

кажется намного лучше.