被咱們忽略的HttpSession線程安全問題

1. 背景java

最近在讀《Java concurrency in practice》(Java併發實戰),其中1.4節提到了Java web的線程安全問題時有以下一段話: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 中的對象必須是線程安全的。含義有兩點:apache

1)Servlet, JSP, Servlet filter 必須是線程安全的(JSP的本質其實就是servlet);安全

2)保存在ServletContext、HttpSession中的對象必須是線程安全的;session

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. 源碼分析
app

下面分析一下爲何將一個這樣的Java對象保存在HttpSession中是有問題的,至少在線程安全方面不嚴謹的,可能會出現併發問題。ide

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中的:this

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。

相關文章
相關標籤/搜索