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在第一行的创建或检索是不是完全是原子的?按原子,我的意思是如果两个Threads调用request.getSession(),一个将被阻塞.虽然都会返回相同的HttpSession实例.因此,如果一个客户端(移动/网络浏览器)进行两个或者调用同一个Servlet(执行上面的代码片段),那么不会有不同的线程看到不同的购物车的值.
假设我确信这不是线程安全的,那么这个线程如何安全呢? 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!
解决方法
ShoppingCart cart = cartRef.get(); if (cart == null) { cart = new ShoppingCart(...); session.setAttribute("shoppingCart",new AtomicReference<ShoppingCart>(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()方法之间发生关系.所以这显然不能保证线程安全,并且可能会在不同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); } }
上述代码执行完毕后,会话被初始化. AtomicReference保证在会话中,并以线程安全的方式.您可以更新同一个同步块中的购物车对象(并将AtomicReference全部放在一起 – 只需将购物车本身放入会话),也可以使用前面显示的代码更新AtomicReference.哪个更好取决于需要做多少初始化,执行此初始化需要多长时间,是否在同步块中执行所有操作会对性能造成太大的损害(最好用分析器确定,而不是猜测),等等.
通常,在我自己的代码中,我只使用一个同步块,不要使用Goetz的AtomicReference技巧.如果我曾经确定同步在我的应用程序中引起了一个活跃的问题,那么我可能通过使用像AtomicReference技巧这样的技巧,从同步块中移除一些更昂贵的初始化.
参见:Is HttpSession thread safe,are set/get Attribute thread safe operations?