被我们忽略的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
*/
@Override
public 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 null
if (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 attribute
Object unbound = attributes.put(name, value);
// ... ...
}
/**
* Release all object references, and initialize instance variables, in
* preparation for reuse of this object.
*/
@Override
public void recycle() {
// Reset the instance variables associated with this Session
attributes.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 attributes
String 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 attributes
int 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。
被我们忽略的HttpSession线程安全问题的更多相关文章
- HttpSession的线程安全问题及注意事项
摘自http://blog.csdn.net/java2000_net/article/details/2922357 HttpSession session = request.getSessio ...
- javaweb回顾第六篇谈一谈Servlet线程安全问题
前言:前面说了很多关于Servlet的一些基础知识,这一篇主要说一下关于Servlet的线程安全问题. 1:多线程的Servlet模型 要想弄清Servlet线程安全我们必须先要明白Servlet实例 ...
- Servlet线程安全问题
Servlet采用单实例多线程方式运行,因此是线程不安全的.默认情况下,非分布式系统,Servlet容器只会维护一个Servlet的实例,当多个请求到达同一个Servlet时,Servlet容器会启动 ...
- APP缓存数据线程安全问题
问题 一般一个 iOS APP 做的事就是:请求数据->保存数据->展示数据,一般用 Sqlite 作为持久存储层,保存从网络拉取的数据,下次读取可以直接从 Sqlite DB 读取.我们 ...
- 【转】数据存储——APP 缓存数据线程安全问题探讨
http://blog.cnbang.net/tech/3262/ 问题 一般一个 iOS APP 做的事就是:请求数据->保存数据->展示数据,一般用 Sqlite 作为持久存储层,保存 ...
- Servlet线程安全问题(转载)
转载地址:https://www.cnblogs.com/LipeiNet/p/5699944.html 前言:前面说了很多关于Servlet的一些基础知识,这一篇主要说一下关于Servlet的线程安 ...
- Java多线程--线程安全问题的相关研究
在刚刚学线程的时候我们经常会碰到这么一个问题:模拟火车站售票窗口售票.代码如下: package cn.blogs.com.isole; /* 模拟火车站售票窗口售票,假设有50张余票 */ publ ...
- struts2学习笔记--线程安全问题小结
在说struts2的线程安全之前,先说一下,什么是线程安全?这是一个网友讲的, 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码.如果每次运行结果和单线程运行的结果是一样 ...
- iOS中的线程安全问题
为了保证线程安全,不会因为多个线程访问造成资源抢夺,出现的运行结果的偏差问题,我们需要使用到线程同步技术,最常用的就是 @synchronized互斥锁(同步锁).NSLock.dispatch_se ...
随机推荐
- xamarin优化listView.ScrollTo
在xamarinz中关于listview的滚动,我这里有点小优化,我做了一个类似QQ的聊天页面,上面是一个listview,下面时一个editText,当在手机上使用时,发现在android平台下,如 ...
- 【Win10 应用开发】实现数据的增量加载
今天,老周有小故事讲,国庆期间.有一次老周在某站台上候公交车.老周旁边也站满了人,突然,有一位头发弄得像电线杆的小伙子,不知为何,没有先兆地就大笑起来. 老周先是看了那小伙子一眼,他手上没有拿什么东西 ...
- SSRS ----环境配置,没有 ReportServer DB 怎么办?
今天项目进入报表开发阶段,按照习惯,打开报表管理器,发现提示下面的错误: 错误:报表服务器无法打开与报表服务器数据库的连接.所有请求和处理都要求与数据库建立连接. 这是怎么回事儿呢,经过排查,发现数据 ...
- java代码的初始化过程研究
刚刚在ITeye上看到一篇关于java代码初始化的文章,看到代码我试着推理了下结果,虽然是大学时代学的知识了,没想到还能做对.(看来自己大学时掌握的基础还算不错,(*^__^*) 嘻嘻……)但 ...
- WCF学习系列汇总
最近在学习WCF,打算把一整个系列的文章都”写“出来,包括理论和实践,这里的“写”是翻译,是国外的大牛写好的,我只是搬运工外加翻译.翻译的不好,大家请指正,谢谢了.如果觉得不错的话,也可以给我点赞,这 ...
- 在web浏览器上显示室内温度(nodeJs+arduino+socket.io)
上次的nodejs操作arduino入门篇中实现了如何连接arduino.这次我们来实现通过arduino测量室内温度并在浏览器上显示出来. [所需材料] 硬件:LM35温度传感器,arduino u ...
- 解决Bash On Ubuntu On Window安装Zsh无效问题附安装说明
前言 Zsh是一款非常棒的Shell,使用Linux和Mac系统的人,基本上都知道zsh的存在. 问题 在安装完Zsh后,zsh是可以使用的,但是重启之后,又恢复至默认的bash. 我在安装好之后,使 ...
- ETL数据从sqlserver到mysql之间迁移
因近期需要进行sqlserver数据到mysql之间的数据同步.偶然之间发现了这一款工具ELK 一.下载 1.Kettle可以在http://kettle.pentaho.org/网站下载 2.下载的 ...
- C# Linq排序
今天在家看了一下linq,实践了一下书中代码,发现排序和查重的先后顺序太重要了. using System; using System.Collections.Generic; using Syste ...
- python之最强王者(7)——元组(tuple)
1.序列(sequence): 说明:在前面的字符串列表中其实我们已经用到了序列,之所以放到这篇来讲主要是为了承上启下,方便理解和记忆. python的数据访问模型:直接存取 ,序列 ,映射 对非容器 ...