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。