Понимание статьи Гетца о безопасности потоков HttpSession - PullRequest
10 голосов
/ 15 апреля 2009

Ссылаясь на статью Брайана Гетца Все ли веб-приложения с состоянием повреждены? для IBM developerWorks, я хочу сослаться на этот фрагмент кода

HttpSession session = request.getSession(true);
ShoppingCart cart = (ShoppingCart)session.getAttribute("shoppingCart");
if (cart == null) {
    cart = new ShoppingCart(...);
    session.setAttribute("shoppingCart", cart);
}        
doSomethingWith(cart);

Насколько я понимаю, этот код не является поточно-ориентированным, поскольку в нем используется шаблон check-then-act . Но у меня есть сомнения:

Не является ли создание или поиск HttpSession в первой строке полностью атомарным? Под атомарным я подразумеваю, что если два потока вызывают request.getSession(), один будет блокироваться. Хотя оба вернут один и тот же экземпляр HttpSession. Таким образом, если клиент (мобильный / веб-браузер) выполняет два или выполняет вызов одного и того же сервлета (который выполняет приведенный выше фрагмент), вы никогда не получите ситуацию, в которой разные потоки видят разные значения для cart.

Если я убежден, что это НЕ потокобезопасный, как можно сделать этот поток безопасным? AtomicReference будет работать? e.g.:

HttpSession session = request.getSession(true);
AtomicReference<ShoppingCart> cartRef = 
     (<AtomicReference<ShoppingCart>)session.getAttribute("shoppingCart");
ShoppingCart cart = cartRef.get();
if (cart == null) {
    cart = new ShoppingCart(...);
    session.setAttribute("shoppingCart",
         new AtomicReference<ShoppingCart>(cart));
}
doSomethingWith(cart);

Merci!

Ответы [ 4 ]

8 голосов
/ 15 апреля 2009

Ваш код по-прежнему не поддерживает поток:

ShoppingCart cart = cartRef.get();
if (cart == null) {
    cart = new ShoppingCart(...);
    session.setAttribute("shoppingCart",
         new AtomicReference<ShoppingCart>(cart));
}

Это связано с тем, что два потока могут одновременно получить значение cart, равное нулю, создать новые объекты корзины покупок и вставить их в сеанс. Один из них «победит», то есть один будет устанавливать объект, используемый будущими запросами, а другой будет - для этого запроса - использовать совершенно другой объект cart.

Чтобы сделать этот потокобезопасным, вам нужно сделать что-то вроде этого, следуя идиоме из статьи, на которую вы ссылались:

while (true) {
    ShoppingCart cart = cartRef.get();
    if (cart != null) {
        break;
    }
    cart = new ShoppingCart(...);
    if (cartRef.compareAndSet(null, cart))
        break;
} 

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

Для решения той части проблемы, которую Брайан Гетц не рассматривает в статье, а именно, как вы в первую очередь включаете AtomicReference в сессию, есть простой и , вероятно, ( но не гарантировано) потокобезопасный способ сделать это. А именно, реализуйте слушатель сеанса и поместите пустые объекты AtomicReference в сеанс с помощью метода sessionCreated:

public class SessionInitializer implements HttpSessionListener {
  public void sessionCreated(HttpSessionEvent event){
    HttpSession session = event.getSession();
    session.setAttribute("shoppingCart", new AtomicReference<ShoppingCart>());
  }
  public void sessionDestroyed(HttpSessionEvent event){
    // No special action needed
  }
}

Этот метод будет вызываться один раз для каждого сеанса, только при его создании, поэтому это подходящее место для выполнения любой инициализации, необходимой для сеанса. К сожалению, спецификация сервлета не требует наличия отношения случай-до между вызовом sessionCreated() в вашем слушателе и вызовом вашего service() метода. Таким образом, очевидно, что это не гарантирует поточнобезопасность и потенциально может варьироваться в зависимости от поведения различных контейнеров сервлетов.

Таким образом, если есть даже небольшая вероятность того, что в данном сеансе может быть одновременно более одного запроса в полете, это недостаточно безопасно. В конечном счете, в этом случае вам нужно использовать какую-то блокировку для инициализации сеанса. Вы можете сделать что-то вроде этого:

HttpSession session = request.getSession(true);
AtomicReference<ShoppingCart> cartRef;
// Ensure that the session is initialized
synchronized (lock) {
    cartRef = (<AtomicReference<ShoppingCart>)session.getAttribute("shoppingCart");
    if (cartRef == null) {
        cartRef = new AtomicReference<ShoppingCart>();
        session.setAttribute("shoppingCart", cartRef);
    }
}

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

Обычно в своем собственном коде я просто использую синхронизированный блок и не использую трюк Гетца AtomicReference. Если бы я когда-либо определил, что синхронизация вызывает проблемы с живучестью в моих приложениях, то я мог бы потенциально переместить некоторые более дорогие инициализации из синхронизированных блоков, используя приемы типа AtomicReference.

.

См. Также: Безопасен ли поток HttpSession, безопасны ли операции установки / получения атрибута потока?

3 голосов
/ 15 апреля 2009

Не является ли создание или поиск HttpSession в первой строке полностью атомное? Под атомным я подразумеваю, что если два Потоки вызывают request.getSession (), один заблокирует.

Даже если getSession блокируется, как только один поток возвращается с сеансом, блокировка снимается. Во время создания новой корзины другие потоки могут получить блокировку, получить сеанс и обнаружить, что в сеансе пока нет корзины.

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

К сожалению, ваше предлагаемое решение делает то же самое: проверяет объект в сеансе и публикует его при необходимости, но без какой-либо блокировки. Тот факт, что атрибут сеанса является AtomicReference, не имеет значения.

Чтобы сделать это безопасно, вы можете использовать что-то вроде Goetz '"Листинг 5" , где чтение и запись в атрибут сеанса выполняются при синхронизации с общей блокировкой.

HttpSession session = request.getSession();
ShoppingCart cart;
synchronized (lock) {
  cart = (ShoppingCart) session.getAttribute(ATTR_CART);
  if (cart == null) {
    cart = new ShoppingCart();
    session.setAttribute(ATTR_CART, cart);
  }
}

Обратите внимание, что в этом примере предполагается, что ShoppingCart является изменяемым и поточно-ориентированным.

2 голосов
/ 15 апреля 2009

Прошло несколько лет с тех пор, как я что-то сделал с сервлетами Java, поэтому я ухожу из памяти.

Я ожидаю, что проблема безопасности потока здесь в проверке для корзины == ноль. При рассмотрении проблем с потоками нужно понимать, что поток может быть прерван между ЛЮБЫМИ двумя машинными инструкциями (а не просто какой-либо строкой кода). То есть даже

i += 1;

не является потокобезопасным (если я все равно доступен), так как i + = 1 - это (как минимум) две инструкции: добавление и хранилище. Поток может быть прерван между надстройкой и хранилищем, и выживет только одна из надстроек.

То же самое происходит в этом примере. Предположим на мгновение, два потока делают запрос в одном и том же сеансе (например, как предполагает Гетц из запросов фреймов или ajax). Один входит в этот раздел кода, успешно получает HttpSession, а затем пытается получить атрибут ShoppingCart. Однако, поскольку он еще не существует, возвращается ноль. Затем поток прерывается другим запросом, который делает то же самое. Это также становится нулевым. Затем эти два запроса выполняются в любой последовательности, поскольку оба получили нулевую ссылку для атрибута shoppingCart, поскольку корзина не была сохранена в это время, оба потока создадут новый объект Cart и оба попытаются его сохранить. Один потеряет, и эти изменения в Корзине будут потеряны. Таким образом, этот код не является потокобезопасным.

Что касается второй половины вашего вопроса, я не знаком с объектом AtomicReference. Я быстро просмотрел API Java для AtomicReference, и он мог бы работать, но я не уверен. В любом случае. Самое очевидное решение, которое я могу придумать, это использовать монитор. По сути, вы хотите сделать взаимное исключение в части кода get-test-set.

Теперь, если ваш объект корзины является атомарным (т. Е. Нам нужно только защитить его получение и настройку, я думаю, что-то подобное может сработать:

public syncronized ShoppingCart atomicGetCart(HttpSession session){    
    ShoppingCart cart = (ShoppingCart)session.getAttribute("shoppingCart");
    if (cart == null) {
        cart = new ShoppingCart(...);
        session.setAttribute("shoppingCart", cart);
    }

    return cart;
}

HttpSession session = request.getSession(true);
ShoppingCart cart = atomicGetCart
doSomethingWith(cart);

Теперь я не очень разбираюсь в производительности мониторов Java, поэтому я не уверен, какие издержки это может повлечь за собой. Кроме того, это должно быть единственное место, где можно получить корзину. По сути, ключевое слово syncronized означает, что только один поток может одновременно вводить метод atomicGetCart. Для обеспечения этого используется блокировка (блокировка - это просто объект, которым может одновременно владеть только один поток). Таким образом, у вас больше не будет состояния гонки, которое было в другом коде.

Надеюсь, это поможет, -Daniel

1 голос
/ 22 июня 2010

Не хочу пересекать пост, но я написал комментарий к этой статье и не получил ответа от автора. Глядя на другие статьи Брайана Гетца на сайте IBM, кажется, что он не хочет ничего отвечать.

Я думаю, что код, который он предложил в листинге 5 своей статьи, не работает.

Предположим, что текущая максимальная оценка составляет 1000, и 2 параллельных запроса с оценками 1100 и 1200 находятся в процессе. Оба запроса одновременно получают самый высокий балл:

PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");

что заставляет оба потока видеть hs как 1000. После этого один из потоков входит в синхронизированную секцию, если условие выполнено, новое значение (скажем, 1200) устанавливается в атрибут servletcontext, и секция синхронизации заканчивается. Затем второй поток входит в синхронизированную секцию, и он все еще видит предыдущее значение hs - hs все еще равно 1000. Если выполняется условие (уверен, что это с 1100> 1000), новое значение (1100) устанавливается в servletcontext. Не должен

PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");

принадлежат синхронизированной секции?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...