HttpSession
Java EE 6 中定义的接口提供了一种用户操作分组方式、识别用户执行的连续操作以及在多个页面请求中存储有关该用户的信息。
总体概述
由于会话显然在许多请求之间共享,因此引发了线程安全问题及其影响。在 Servlet 3.0 规范的第 7 章关于 Sessions 中,人们可能会发现处理此类问题的重要性的支持:
多个执行请求线程的 servlet 可以同时对同一个会话对象进行主动访问。容器必须确保以线程安全的方式执行表示会话属性的内部数据结构的操作。开发人员负责对属性对象本身进行线程安全访问。这将保护 HttpSession 对象内的属性集合免受并发访问,从而消除应用程序导致该集合损坏的机会。
Servlet 3.0 规范 (JSR-315),ch。7.7.1,强调我的。
但是,所有这些混乱从何而来?在 AJAX 之前的时代,Web 应用程序开发人员并不太关心同步,因为同一用户同时访问会话的可能性很小。但是随着构建支持 AJAX 的 Web 应用程序的趋势不断上升,来自同一用户的两个请求很可能同时出现,因此也很可能同时访问会话。
作为旁注,值得注意的是,使用SingleThreadModel
servlet 的接口可以稍微降低线程问题,但对其应用的必要性存在争议(Servlet 3.0 Specification (JSR-315), ch. 2.2.1)。此外,它已被弃用,因为 Servlet 3.0 和“一次可访问多个 servlet 实例的对象,例如 HttpSession 的实例,可能在任何特定时间对多个 servlet 可用,包括那些实现 SingleThreadModel 的对象”。
同步问题
Java EE 6 教程明确指出“可能出现并发访问......当多个 Web 组件访问存储在会话中的对象时”(Java EE 6 教程,第 II-15 章)。此外,如果我们仔细观察HttpSession
接口,我们会发现一些允许将对象绑定到会话的方法,本质上提供了跨多个用户连接存储用户信息的可能性,从而克服了 HTTP 协议的无状态性。这些方法是:
getAttribute(String name)
(以及现已弃用的getValue(String name)
);
setAttribute(String name, Object value)
(以及现已弃用的putValue(String name, Object value)
;
removeAttribute(String name)
(以及现已弃用的removeValue(String name)
);
invalidate()
和
- 接口的其他方法。
最后一个方法使这个会话无效并解除绑定任何对象,持有用户信息,绑定到它,所以它不是我们害怕的。最重要的方法是Object
从会话中读取、写入和删除 s 的方法,因为这些方法将被不同的线程同时调用以处理/访问数据。
正如 Brian Goetz 在Java 理论和实践中提到的,并发访问的明显问题是:所有有状态的 Web 应用程序都被破坏了吗?:
- 原子性故障,其中一个线程正在更新多个数据,而另一个线程在它们处于不一致状态时读取数据,并且
- 读取线程和写入线程之间的可见性失败,其中一个线程修改数据,但另一个线程看到过时的数据或处于不一致状态的数据。
问题的简单解决方案
他后来提出了 5 种减少 Web 应用程序中并发问题的技术,最后指出“在 HttpSession 上序列化请求可以消除许多并发风险”。Marty Hall 在他关于会话跟踪的在线教程中提出了以下关于同步的建议:“使用会话或会话中的值作为同步块的标签”。所以,基本设置是:
HttpSession session = request.getSession();
synchronized(session) {
SomeClass value = (SomeClass)session.getAttribute("someID");
if (value == null) {
value = new SomeClass(...);
}
doSomethingWith(value);
session.setAttribute("someID", value);
}
使用此设置,访问会话的重叠请求将被同步。
非线程安全用法示例
HttpSession session = request.getSession();
MyClass myClass = (MyClass)session.getAttribute("myClass");
if(myClass != null) {
myClass.performOperation();
session.setAttribute("myClass", myClass);
}
JSF 中对 session 的显式操作的需要
很明显,在会话对象中操作数据可能会导致并发问题。此外,当您选择在将为您隐式管理会话对象的 JSF 框架内进行开发时,它的适用性是值得怀疑的。
最后,您应该将对象放在会话中,而它们本来就属于那里。开发人员有时倾向于将对象放入会话中,以按照他们的方式解决问题,但通常有更好的方法。Thomas Asel的文章JSF Best Practices: Scope management for clean session中涵盖了一些错误。
缓解线程问题的一些方法
HttpSession
当使用错误的方式时,大多数线程问题都会出现。从这个角度来看,线程问题是作用域问题的结果。
例如,如果您将一个值放入应该属于较窄范围(即请求范围或视图范围)的会话中,那么您的代码就会很容易受到有形概率的并发问题的影响。相反,如果您的所有会话数据都属于正确的范围,那么用户遇到并发问题的可能性就会非常低。
@SessionScoped
bean 线程安全必须由开发人员确保,只要 JSF 最终将这些 bean 存储为属性,HttpSession
并以托管 bean 的名称作为键。通过名称访问托管 bean 是 JSF 中的一种便利,据我所知,这是由session.getAttribute("managedBeanName")
.
在这种情况下,如果用户在会话中放置了正确的数据并正确管理它(我的意思是没有解决应该在没有会话干扰的情况下解决的问题),那么我看到的唯一缺点是在 BalusC 的回答中提到的,即引用“紧密耦合和糟糕的设计”。否则(如果我们忽略JSF 在幕后正确管理 bean 的生命周期和糟糕的设计问题),用法是类似的。
我想到了一些导致并发问题的范围问题示例:
- 存储与会话中的用户信息无关的任何内容(用户身份验证详细信息、他的偏好、国际化 Web 应用程序中的语言环境选择;
- 视图之间的信息共享不应该使用会话来完成:数据的检索应该通过
GET
请求来完成,数据的操作应该通过POST
请求来完成;在页面转发过程中传递数据可以通过例如 using<f:setPropertyActionListener>
来完成,在页面重定向过程中传递数据可以通过例如使用#{flash}
属性来完成;
- 在重复的组件设置中传递信息应该在不干扰会话的情况下完成,通过
<f:attribute>
、<f:setPropertyActionListener>
、<f:param>
和信息检索 - 通过操作(侦听器)方法<f:viewParam>
等。
总而言之,如果您不严格遵循上述列表,您当然有一天会看到错位的输出,但如果您选择正确的数据属于会话范围,则几乎不会看到任何问题。通常,可以在回答整个用户会话问题期间所需的信息时做出这种数据选择。
话虽如此,一个常见的错误是将信息放入会话中以使其在后续视图中可用,检索该信息然后将其从会话中删除,这显然是错误的。我坚信这种方法是由您的对话者采取的。
最后,JSF 正在发展以促进开发任务,所以为什么不使用它的有用特性呢!