Возможно ли синхронизация внутри 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
Я нашел это приятное объяснение в spring-mvc 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;
кажется намного лучше.