【问题标题】:Understanding Goetz's article on Thread safety of HttpSession理解Goetz关于HttpSession的线程安全的文章
【发布时间】:2009-04-15 03:06:05
【问题描述】:

参考Brian Goetz 为IBM developerWorks 撰写的文章Are all stateful Web applications broken?,我想参考这段代码

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 实例。因此,如果客户端(移动/Web 浏览器)进行两次或调用同一个 Servlet(执行上面的 sn-p),您将永远不会遇到不同线程看到 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);

谢谢!

【问题讨论】:

    标签: java multithreading servlets concurrency


    【解决方案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对象。

    为了解决 Brian Goetz 在文章中没有解决的部分问题,即如何首先让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() 方法之间存在 happens-Before 关系。因此,这显然不能保证是线程安全的,并且不同 Servlet 容器之间的行为可能会有所不同。

    因此,如果给定会话一次有多个请求在进行中的可能性甚至很小,这也是不够安全的。最终,在这种情况下,您需要使用某种锁来初始化会话。你可以这样做:

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

    在上面的代码执行之后,你的 Session 就被初始化了。 AtomicReference 保证在会话中,并且以线程安全的方式。您可以在同一个同步块中更新购物车对象(并省去AtomicReference 一起——只需将购物车本身放入会话中),或者您可以使用上面显示的代码更新AtomicReference。哪个更好取决于您需要进行多少初始化,执行此初始化需要多长时间,以及在同步块中执行 everything 是否会对性能造成太大影响(最好使用profiler,而不是猜测)等等。

    通常,在我自己的代码中,我只使用同步块而不使用 Goetz 的 AtomicReference 技巧。如果我确定同步会导致我的应用程序出现活性问题,那么我可能会使用 AtomicReference 之类的技巧将一些更昂贵的初始化从同步块中移出。

    另请参阅:Is HttpSession thread safe, are set/get Attribute thread safe operations?

    【讨论】:

    • 如何安全地将 cartRef 放入会话中?看来您仍然需要像session.setAttribute("shoppingCart", cartRef) 这样的电话。如何防止会破坏交错线程的 cartRef 的竞争条件?
    • @erickson:我扩展了我的答案以讨论如何将初始值放入会话中。
    • 我考虑过使用监听器或检查会话的 isNew 位,但我不确定规范是否保证在通过请求使会话可用之前完成所有监听器。 (多年来与不稳定的 WebLogic 会话听众作斗争让我很担心。)你有引用吗?
    【解决方案2】:

    不是创建或检索 第一行中的 HttpSession 完全 原子?原子,我的意思是如果两个 线程调用request.getSession(),一 会阻塞。

    即使getSession 阻塞,只要一个线程与会话一起返回,锁就会被放弃。在创建新购物车时,其他线程能够获取锁,获取会话,然后发现会话中还没有购物车。

    所以,这段代码不是线程安全的。存在一种竞争条件,很容易导致为单个会话创建多个 ShoppingCarts

    不幸的是,您提出的解决方案正在做完全相同的事情:检查会话中的对象,并在需要时发布一个对象,但没有任何锁定。 session 属性是AtomicReference 的事实并不重要。

    要安全地执行此操作,您可以使用Goetz' "Listing 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;
      

      不是线程安全的(如果 i 无论如何都是共享的),因为 i += 1 是(至少)两条指令:添加和存储。线程可以在 add 和 store 之间中断,并且只有一个 add 会存活。

      在这个例子中也发生了同样的事情。假设有片刻,两个线程在同一个会话上发出请求(例如,正如 Goetz 从帧或 ajax 请求中建议的那样)。进入此代码部分,成功检索 HttpSession,然后尝试获取“shoppingCart”属性。但是,由于它尚不存在,因此返回 null。然后线程被另一个做同样事情的请求中断。它也为空。然后,这两个请求以任意顺序进行,但是,由于当时尚未存储购物车,因此两个请求都检索了“shoppingCart”属性的空引用,因此两个线程都将创建一个新的购物车对象,并且都将尝试存储它。一个会松动,对购物车的这些更改将丢失。因此这段代码不是线程安全的。

      至于你问题的后半部分,我对 AtomicReference 对象不熟悉。我快速查看了 AtomicReference 的 java API,它可能会起作用,但我不确定。任何状况之下。我能想到的最明显的解决方案是使用监视器。基本上,您要做的是对代码的 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。锁用于强制执行此操作(锁只是一个对象,一次只能由一个线程拥有)。这样你就不再有其他代码中的竞争条件了。

      希望这会有所帮助, -丹尼尔

      【讨论】:

      • 你在那里锁定什么对象?
      • 对不起,我应该澄清这一点。简单地说,您可以创建一个单例 Manager 对象并将其锁定。但这会阻止所有请求(一个锁)。更好的解决方案可能是继承 HttpSession,并锁定它。但是-我不太记得 servlet,可能有更好的方法
      【解决方案4】:

      不想发帖,但我对这篇文章发表了评论,但没有得到作者的答复。看看 Brian Goetz 在 IBM 网站上的其他文章,他似乎并不热衷于回答任何问题。

      我认为他在文章清单 5 中提出的代码已损坏。

      假设当前的最高分数是 1000,并且有 2 个分数分别为 1100 和 1200 的并发请求正在进行中。两个请求同时检索最高分:

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

      是什么让两个线程都将 hs 视为 1000。 之后,其中一个线程进入同步部分,如果满足条件,则将新值(比如 1200)设置为 servletcontext 属性,同步部分结束。 然后第二个线程进入同步部分,它仍然看到以前的 hs 值 - hs 仍然是 1000。如果满足 contition(确定它是从 1100>1000 开始),则将新值 (1100 ) 设置为 servletcontext。 不应该

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

      属于同步部分?

      【讨论】:

      • 没有。 ctx.getAttribute("highScore") 没有获得高分value,它获得了高分对象的reference。当对象的属性发生更改时,无论您使用哪个引用来访问该对象,只要您没有可见性风险(由同步块处理),就会看到它。在您描述的情况下,第二个线程永远看不到 1000;它会看到 1200,即使在调用 ctx.getAttribute 时它仍然是 1000。
      猜你喜欢
      • 2011-07-21
      • 1970-01-01
      • 2014-01-09
      • 2021-10-01
      • 2010-10-11
      • 2011-11-25
      • 2012-07-14
      • 1970-01-01
      • 2019-10-19
      相关资源
      最近更新 更多