Понимание статьи Goetz о безопасности потоков HttpSession

Ссылаясь на статью Брайана Гетца Разве все веб-приложения с сохранением состояния нарушены? для 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? например:.

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!

Ответ 1

Ваш код по-прежнему не является потокобезопасным:

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
  }
}

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

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

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

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

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

Ответ 2

Не создание или извлечение HttpSession в первой строке полностью атомное? По атомному, я имею в виду, что если два Threads call 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 является изменяемым и потокобезопасным.

Ответ 3

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

Я бы ожидал, что проблема безопасности потока здесь находится в проверке на cart == null. При рассмотрении проблем с потоками необходимо понять, что поток может быть прерван между любыми двумя машинными инструкциями (а не только с любой строкой кода). Иными словами, даже

i += 1;

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

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

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

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

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

Ответ 4

Не хочу переписывать пост, но я написал комментарий к этой статье и не получил ответа от автора. Глядя на Брайана Гетца, другие статьи на сайте 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");

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