眾所周知,Servlet為單實例多線程,非線程安全的。
若一個Servlet對應多個URL映射,那么將會生成一個還是多個Servlet實例呢?
最好的辦法就是動手體驗一下
@WebServlet({ "/demoServlet1", "/demoServlet2" })
public class DemoServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
PrintWriter out = response.getWriter();
out.println(request.getServletPath() + "[" + this + "]");
out.flush();
out.close();
}
}
輸出結果:
/demoServlet1[com.learn.servlet3.thread.DemoServlet@1320a41]
/demoServlet2[com.learn.servlet3.thread.DemoServlet@9abce9]
輸出結果可以看到映射/demoServlet1和/demoServlet2對應Servlet實例是不同的。
結果證明:Servlet將為每一個URL映射生成一個實例;一個Servlet可能存在多個示例,但每一個實例都會對應不同的URL映射。
下面討論單個Servlet、多線程情況下保證數據線程同步的幾個方法。
- synchronized:代碼塊,方法
大家都會使用的方式,不用詳細介紹了。
建議優先選擇修飾方法。
- volatile
輕量級的鎖,可以保證多線程情況單線程讀取所修飾變量時將會強制從共享內存中讀取最新值,但賦值操作并非原子性。
一個具有簡單計數功能Servlet示范:
/**
* 使用Volatile作為輕量級鎖作為計數器
*
* @author yongboy
* @date 2011-3-12
* @version 1.0
*/
@WebServlet("/volatileCountDemo")
public class VolatileCountServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private volatile int num = 0;
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
addOne();
response.getWriter().write("now access num : " + getNum());
}
/**
* 讀取開銷低
*/
private int getNum() {
return num;
}
/**
* 其寫入為非線程安全的,賦值操作開銷高
*/
private synchronized void addOne() {
num ++;
}
}
我們在為volatile修飾屬性賦值時,還是加把鎖的。
- ThreadLocal
可以保證每一個線程都可以獨享一份變量副本,每個線程可以獨立改變副本,不會影響到其它線程。
這里假設多線程環境一個可能落顯無聊的示范,初始化一個計數,然后循環輸出:
@WebServlet("/threadLocalServlet")
public class ThreadLocalServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static ThreadLocal threadLocal = new ThreadLocal() {
@Override
protected Integer initialValue() {
return 0;
}
};
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.setHeader("Connection", "Keep-Alive");
PrintWriter out = response.getWriter();
out.println("start... " + " [" + Thread.currentThread() + "]");
out.flush();
for (int i = 0; i < 20; i++) {
out.println(threadLocal.get());
out.flush();
threadLocal.set(threadLocal.get() + 1);
}
// 手動清理,當然隨著當前線程結束,亦會自動清理調
threadLocal.remove();
out.println("finish... ");
out.flush();
out.close();
}
}
若創建一個對象較為昂貴,但又是非線程安全的,在某種情況下要求僅僅需要在線程中獨立變化,不會影響到其它線程。選擇使用ThreadLocal較好一些,嗯,還有,其內部使用到了WeakHashMap,弱引用,當前線程結束,意味著創建的對象副本也會被垃圾回收。
Hibernate使用ThreadLocal創建Session;Spring亦用于創建對象會使用到一點。
嗯,請注意這不是解決多線程共享變量的鑰匙,甚至你想讓某個屬性或對象在所有線程中都保持原子性,顯然這不是解決方案。
- Lock
沒什么好說的,現在JDK版本支持顯式的加鎖,相比synchronized,添加與釋放更加靈活,功能更為全面。
@WebServlet("/lockServlet")
public class LockServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static int num = 0;
private static final Lock lock = new ReentrantLock();
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try{
lock.lock();
num ++;
response.getWriter().println(num);
}finally{
lock.unlock();
}
}
}
必須手動釋放鎖,否則將會一直鎖定。
- wait/notify
較老的線程線程同步方案,較之Lock,不建議再次使用。
- 原子操作
原子包裝類,包括一些基本類型(int, long, double, boolean等)的包裝,對象屬性的包裝等。
@WebServlet("/atomicServlet")
public class AtomicServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final AtomicInteger num = new AtomicInteger(0);
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
PrintWriter out = response.getWriter();
out.println(num.incrementAndGet());
out.flush();
out.close();
}
}
包裝類提供了很多的快捷方法,比如上面的incrementAndGet方法,自身增加1,然后返回結果值,并且還是線程安全的,省缺了我們很多手動、笨拙的編碼實現。
更多原子類,請參見 java.util.concurrent.atomic包。
- 一些建議
盡量不要在Servlet中單獨啟用線程
使用盡可能使用局部變量
盡可能避免使用鎖
- 數據結構小結
在平常環境下使用的一些數據結構,在多線程并發環境下可選擇使用java.util.concurrent里內置替代品。下面有一個小結,僅供參考。
非線程安全 | 工具版 | JUC版本 |
HashMap |
Collections.synchronizedMap |
ConcurrentHashMap |
ArrayList |
Collections.synchronizedList |
CopyOnWriteArrayList |
HashSet |
Collections.synchronizedSet |
synchronizedSet |
Queue |
|
ConcurrentLinkedQueue |
Servlet線程安全有很太多的話題要說,以上僅僅為蜻蜓點水,真正需要學習和實踐的還有一段長路要學習。另,話說,這里已和Servlet版本無關,是知識積累和分享。