java session 安全_被我们忽略的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 classUser {private intid;privateString userName;privateString password;//... ...

public intgetId() {returnid;

}public void setId(intid) {this.id =id;

}publicString getUserName() {returnuserName;

}public voidsetUserName(String userName) {this.userName =userName;

}publicString getPassword() {returnpassword;

}public voidsetPassword(String password) {this.password =password;

}

}

2. 源码分析

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

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

public class StandardSession implementsHttpSession, Session, Serializable {//----------------------------------------------------- Instance Variables

/*** The collection of user data attributes associated with this Session.*/

protected Map attributes = new ConcurrentHashMap<>();/*** Return the object bound with the specified name in this session, or

* null if no object is bound with that name.

*

*@paramname Name of the attribute to be returned

*

*@exceptionIllegalStateException if this method is called on an

* invalidated session*/@OverridepublicObject getAttribute(String name) {if (!isValidInternal())throw newIllegalStateException

(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.

*

* After this method executes, and if the object implements

* HttpSessionBindingListener, the container calls

* valueBound() on the object.

*

*@paramname Name to which the object is bound, cannot be null

*@paramvalue Object to be bound, cannot be null

*@paramnotify whether to notify session listeners

*@exceptionIllegalArgumentException if an attempt is made to add a

* non-serializable object in an environment marked distributable.

*@exceptionIllegalStateException if this method is called on an

* invalidated session*/

public void setAttribute(String name, Object value, booleannotify) {//Name cannot be null

if (name == null)throw newIllegalArgumentException

(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.*/@Overridepublic voidrecycle() {//Reset the instance variables associated with this Session

attributes.clear();//... ...

}/*** Write a serialized version of this session object to the specified

* object output stream.

*

* IMPLEMENTATION NOTE: The owning Manager will not be stored

* in the serialized representation of this Session. After calling

* readObject(), you must set the associated Manager

* explicitly.

*

* IMPLEMENTATION NOTE: 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 distributable property of the

* associated Manager is set to true.

*

*@paramstream The output stream to write to

*

*@exceptionIOException if an input/output error occurs*/

protected void doWriteObject(ObjectOutputStream stream) throwsIOException {//... ...//Accumulate the names of serializable and non-serializable attributes

String keys[] =keys();

ArrayList saveNames = new ArrayList<>();

ArrayList saveValues = new ArrayList<>();for (int i = 0; i < keys.length; i++) {

Object value=attributes.get(keys[i]);if (value == null)continue;else if ( (value instanceofSerializable)&& (!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 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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值