被我们忽略的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。

相关推荐
静听山水2 分钟前
Hive 的数据存储单元结构
hive
大数据编程之光3 分钟前
Hive 查询各类型专利 top10 申请人及专利申请数
大数据·数据仓库·hive·hadoop
杰克逊的日记6 分钟前
Hive详解
数据仓库·hive·hadoop
进击的六角龙27 分钟前
Python中处理Excel的基本概念(如工作簿、工作表等)
开发语言·python·excel
一只爱好编程的程序猿1 小时前
Java后台生成指定路径下创建指定名称的文件
java·python·数据下载
Aniay_ivy1 小时前
深入探索 Java 8 Stream 流:高效操作与应用场景
java·开发语言·python
gonghw4031 小时前
DearPyGui学习
python·gui
向阳12181 小时前
Bert快速入门
人工智能·python·自然语言处理·bert
engchina1 小时前
Neo4j 和 Python 初学者指南:如何使用可选关系匹配优化 Cypher 查询
数据库·python·neo4j
兆。1 小时前
掌握 PyQt5:从零开始的桌面应用开发
开发语言·爬虫·python·qt