被我们忽略的HttpSession线程安全问题

1. 背景

最近在读《Java concurrency in practice》(Java并发实战),其中1.4节提到了Java web的线程安全问题时有如下一段话:

Servlets and JPSs, as well as servlet filters and objects stored in scoped containers like ServletContext and HttpSession, 
simply have to be thread-safe.

Servlet, JSP, Servlet filter 以及保存在 ServletContext、HttpSession 中的对象必须是线程安全的。含义有两点:

1)Servlet, JSP, Servlet filter 必须是线程安全的(JSP的本质其实就是servlet);

2)保存在ServletContext、HttpSession中的对象必须是线程安全的;

servlet和servelt filter必须是线程安全的,这个一般是不存在什么问题的,只要我们的servlet和servlet filter中没有实例属性或者实例属性是”不可变对象“就基本没有问题。但是保存在ServletContext和HttpSession中的对象必须是线程安全的,这一点似乎一直被我们忽略掉了。在Java web项目中,我们经常要将一个登录的用户保存在HttpSession中,而这个User对象就是像下面定义的一样的一个Java bean:

复制代码

public class User {private int id;private String userName;private String password;// ... ...public int getId() {return id;}public void setId(int id) {this.id = id;}public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}
}

复制代码

2. 源码分析

下面分析一下为什么将一个这样的Java对象保存在HttpSession中是有问题的,至少在线程安全方面不严谨的,可能会出现并发问题。

Tomcat8.0中HttpSession的源码在org.apache.catalina.session.StandardSession.java文件中,源码如下(截取我们需要的部分):

复制代码

public class StandardSession implements HttpSession, Session, Serializable {// ----------------------------------------------------- Instance Variables/*** The collection of user data attributes associated with this Session.*/protected Map<String, Object> attributes = new ConcurrentHashMap<>();/*** Return the object bound with the specified name in this session, or* <code>null</code> if no object is bound with that name.** @param name Name of the attribute to be returned** @exception IllegalStateException if this method is called on an*  invalidated session*/@Overridepublic Object  getAttribute(String name) {if (!isValidInternal())throw new IllegalStateException(sm.getString("standardSession.getAttribute.ise"));if (name == null) return null;return (attributes.get(name));}/*** Bind an object to this session, using the specified name.  If an object* of the same name is already bound to this session, the object is* replaced.* <p>* After this method executes, and if the object implements* <code>HttpSessionBindingListener</code>, the container calls* <code>valueBound()</code> on the object.** @param name Name to which the object is bound, cannot be null* @param value Object to be bound, cannot be null* @param notify whether to notify session listeners* @exception IllegalArgumentException if an attempt is made to add a*  non-serializable object in an environment marked distributable.* @exception IllegalStateException if this method is called on an*  invalidated session*/public void setAttribute(String name, Object value, boolean notify) {// Name cannot be nullif (name == null)throw new IllegalArgumentException(sm.getString("standardSession.setAttribute.namenull"));// Null value is the same as removeAttribute()if (value == null) {removeAttribute(name);return;}// ... ...// Replace or add this attributeObject unbound = attributes.put(name, value);// ... ...}/*** Release all object references, and initialize instance variables, in* preparation for reuse of this object.*/@Overridepublic void recycle() {// Reset the instance variables associated with this Sessionattributes.clear();// ... ...}/*** Write a serialized version of this session object to the specified* object output stream.* <p>* <b>IMPLEMENTATION NOTE</b>:  The owning Manager will not be stored* in the serialized representation of this Session.  After calling* <code>readObject()</code>, you must set the associated Manager* explicitly.* <p>* <b>IMPLEMENTATION NOTE</b>:  Any attribute that is not Serializable* will be unbound from the session, with appropriate actions if it* implements HttpSessionBindingListener.  If you do not want any such* attributes, be sure the <code>distributable</code> property of the* associated Manager is set to <code>true</code>.** @param stream The output stream to write to** @exception IOException if an input/output error occurs*/protected void doWriteObject(ObjectOutputStream stream) throws IOException {// ... ...// Accumulate the names of serializable and non-serializable attributesString keys[] = keys();ArrayList<String> saveNames = new ArrayList<>();ArrayList<Object> saveValues = new ArrayList<>();for (int i = 0; i < keys.length; i++) {Object value = attributes.get(keys[i]);if (value == null)continue;else if ( (value instanceof Serializable)&& (!exclude(keys[i]) )) {saveNames.add(keys[i]);saveValues.add(value);} else {removeAttributeInternal(keys[i], true);}}// Serialize the attribute count and the Serializable attributesint n = saveNames.size();stream.writeObject(Integer.valueOf(n));for (int i = 0; i < n; i++) {stream.writeObject(saveNames.get(i));try {stream.writeObject(saveValues.get(i));  // ... ...            } catch (NotSerializableException e) {// ... ...               }}}
}

复制代码

我们看到每一个独立的HttpSession中保存的所有属性,是存储在一个独立的ConcurrentHashMap中的:

protected Map<String, Object> attributes = new ConcurrentHashMap<>();

所以我可以看到 HttpSession.getAttribute(), HttpSession.setAttribute() 等等方法就都是线程安全的。

另外如果我们要将一个对象保存在HttpSession中时,那么该对象应该是可序列化的。不然在进行HttpSession的持久化时,就会被抛弃了,无法恢复了:

            else if ( (value instanceof Serializable)
                    && (!exclude(keys[i]) )) {
                saveNames.add(keys[i]);
                saveValues.add(value);
            } else {
                removeAttributeInternal(keys[i], true);
            }

所以从源码的分析,我们得出了下面的结论:

1)HttpSession.getAttribute(), HttpSession.setAttribute() 等等方法都是线程安全的;

2)要保存在HttpSession中对象应该是序列化的;

虽然getAttribute,setAttribute是线程安全的了,那么下面的代码就是线程安全的吗?

session.setAttribute("user", user);

User user = (User)session.getAttribute("user", user);

不是线程安全的!因为User对象不是线程安全的,假如有一个线程执行下面的操作:

User user = (User)session.getAttribute("user", user);

user.setName("xxx");

那么显然就会存在并发问题。因为会出现:有多个线程访问同一个对象 user, 并且至少有一个线程在修改该对象。但是在通常情况下,我们的Java web程序都是这么写的,为什么又没有出现问题呢?原因是:在web中 ”多个线程访问同一个对象 user, 并且至少有一个线程在修改该对象“ 这样的情况极少出现;因为我们使用HttpSession的目的是在内存中暂时保存信息,便于快速访问,所以我们一般不会进行下面的操作:

User user = (User)session.getAttribute("user", user);

user.setName("xxx");

我们一般是只使用对从HttpSession中的对象使用get方法来获得信息,一般不会对”从HttpSession中获得的对象“调用set方法来修改它;而是直接调用 setAttribute来进行设置或者替换成一个新的。

3. 结论

所以结论是:如果你能保证不会对”从HttpSession中获得的对象“调用set方法来修改它,那么保存在HttpSession中的对象可以不是线程安全的(因为他是”事实不可变对象“,并且ConcurrentHashMap保证了它是被”安全发布的“);但是如果你不能保证这一点,那么你必须要实现”保存在HttpSession中的对象必须是线程安全“。不然的话,就存在并发问题。

使Java bean线程安全的最简单方法,就是在所有的get/set方法都加上synchronized。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/284500.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java+Swing: 删除数据 整理15

1. 添加点击事件 2. 在MainViewHandler处理类中&#xff0c;实现相应的处理操作 if ("删除".equals(text)){int[] selectedRowIds mainView.getSelectedRowIds();if (selectedRowIds.length 0){JOptionPane.showMessageDialog(mainView, "请选择要删除的数据…

JWT令牌的作用和生成

JWT令牌&#xff08;JSON Web Token&#xff09;是一种用于身份验证和授权的安全令牌。它由三部分组成&#xff1a;头部、载荷和签名。 JWT令牌的作用如下&#xff1a; 身份验证&#xff1a;JWT令牌可以验证用户身份。当用户登录后&#xff0c;服务器会生成一个JWT令牌并返回…

Linux - 非root用户使用systemctl管理服务

文章目录 方式一 &#xff08;推荐&#xff09;1. 编辑sudoers文件&#xff1a;2. 设置服务文件权限&#xff1a;3. 启动和停止服务&#xff1a; 方式二1. 查看可用服务&#xff1a;2. 选择要配置的服务&#xff1a;3. 创建自定义服务文件&#xff1a;4. 重新加载systemd管理的…

【网络安全】-Linux操作系统—操作系统发展历史与Linux

文章目录 操作系统发展历史初期的操作系统分时操作系统个人计算机操作系统 Linux的诞生UNIX与GNU项目Linux内核的创建 Linux的特点开放源代码多样性社区支持 Linux的应用服务器和超级计算机嵌入式系统桌面系统 总结 操作系统发展历史 操作系统&#xff08;Operating System&am…

Leetcode sql50基础题最后的4题啦

算是结束了这个阶段了&#xff0c;之后的怎么学习mysql的方向还没确定&#xff0c;但是不能断掉&#xff0c;而且路是边走边想出来的。我无语了写完了我点进去看详情都不让&#xff0c;还得重新开启计划&#xff0c;那我之前的题解不都没有了&#xff01;&#xff01; 1.第二高…

DDD | 入门 - [概念体系]

INDEX 接触 DDD 前的准备不要用和 MVC 对照的思想去接触 DDD 领域 & 子域 & 界限上下文思路领域子域界限上下文 领域的初步划分 接触 DDD 前的准备 不要用和 MVC 对照的思想去接触 DDD 不要用和 MVC 对照的思想去接触 DDD&#xff0c;这样你会很痛苦。 在之前 蛋式编…

Linux下Netty实现高性能UDP服务

前言 近期笔者基于Netty接收UDP报文进行业务数据统计的功能&#xff0c;因为Netty默认情况下处理UDP收包只能由一个线程负责&#xff0c;无法像TCP协议那种基于主从reactor模型实现多线程监听端口&#xff0c;所以笔者查阅网上资料查看是否有什么方式可以接收UDP收包的性能瓶颈…

IDEA报错处理

问题1 IDEA 新建 Maven 项目没有文件结构 pom 文件为空 将JDK换成1.8后解决。 网络说法&#xff1a;别用 java18&#xff0c;换成 java17 或者 java1.8 都可以&#xff0c;因为 java18 不是 LTS 版本&#xff0c;有着各种各样的问题。。

手拉手EasyExcel极简实现web上传下载(全栈)

环境介绍 技术栈 springbootmybatis-plusmysqleasyexcel 软件 版本 mysql 8 IDEA IntelliJ IDEA 2022.2.1 JDK 1.8 Spring Boot 2.7.13 mybatis-plus 3.5.3.2 EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。 他能让你在不用考虑性…

OpenSergo Dubbo 微服务治理最佳实践

*作者&#xff1a;何家欢&#xff0c;阿里云 MSE 研发工程师 Why 微服务治理&#xff1f; 现代的微服务架构里&#xff0c;我们通过将系统分解成一系列的服务并通过远程过程调用联接在一起&#xff0c;在带来一些优势的同时也为我们带来了一些挑战。 如上图所示&#xff0c;可…

C++学习笔记(十二)------is_a关系(继承关系)

你好&#xff0c;这里是争做图书馆扫地僧的小白。 个人主页&#xff1a;争做图书馆扫地僧的小白_-CSDN博客 目标&#xff1a;希望通过学习技术&#xff0c;期待着改变世界。 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 文章目录 前言 一、继承关系…

HIVE窗口函数

什么是窗口函数 hive中开窗函数通过over关键字声明&#xff1b;窗口函数&#xff0c;准确地说&#xff0c;函数在窗口中的应用&#xff1b;比如sum函数不仅可在group by后聚合&#xff0c;在可在窗口中应用&#xff1b; hive中groupby算子和开窗over&#xff0c;shuffle的逻辑…