Возможна ли синхронизация внутри HttpSession? - PullRequest
29 голосов
/ 21 марта 2012

ОБНОВЛЕНИЕ: Решение сразу после вопроса.

Вопрос:

Обычно синхронизация заключается в сериализации параллельных запросов в 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) {
  ...
}

Есть какие-либо комментарии к этому решению?

Ответы [ 9 ]

27 голосов
/ 21 марта 2012

Я нашел это хорошее объяснение в 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 сам объект.

См. также

8 голосов
/ 21 марта 2012

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

2 голосов
/ 26 октября 2013

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

2 голосов
/ 21 марта 2012

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

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

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

1 голос
/ 20 июля 2015

Лично я реализую сессионную блокировку с помощью 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 голос
/ 07 июля 2015

В книге «Java-сервлеты и JSP (3-е издание)» Murach предлагается другое решение:

Cart cart;
final Object lock = request.getSession().getId().intern();
synchronized (lock) {
    cart = (Cart) session.getAttribute("cart");
}
1 голос
/ 21 марта 2012

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

Примечания:

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

Использование

private static final Object LOCK = new Object();

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

Это нужно изменить.

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

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

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

0 голосов
/ 16 января 2013

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

...