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。