如題,是嗎?首先我們得搞清楚啥是單例。一聊起單例,條件反射的第一個想到的自然是單例模式。單例模式的定義:一個類有且僅有一個實例,並且自行實例化向整個系統提供。如果按照Java中單例的定義,那么當Servlet沒有實現SingleThreadModel接口時,它確實是單例的。
但如果往細處再進一步深究的話,又好像不是那么一回事了。還是先看單例模式,舉個例子,Sigleton類是個單例,它為了保證實例的唯一性,堅決不給別人實例化它的機會,那么它會把構造器定義為私有的(private),這樣其他人就沒法new出它的實例了(但其實通過反射還是可以實例化的,這里不展開)。而Servlet本身是一個接口,我們一般用的是HttpServlet,它繼承了GenericServlet,而GenericServlet實現了Servlet。雖然HttpServlet是抽象類,然而它卻有自己的構造器,而且是公有的(public)。我們知道子類初始化實例時必然先調用父類的構造器,也就是如果我現在有一個DemoServlet,那么實例化它將執行HttpServlet的構造器。當然了,父親的父親GenericServlet的構造器也會加載,而且這位祖父的構造器也是公有的。如此看來,單例模式中的私有構造器與Servlet中的公有構造器明顯匹配不上了。
綜上所述,我還是偏向於廣義上的范疇,只要滿足在整個系統中僅有一個實例,就認為它是單例。回到最先前的那句話:當Servlet沒有實現SingleThreadModel接口時,它才是單例的。雖然SingleThreadModel被標記為過期的了,但仍可以用。如果實現該接口,那么每次請求相同的Servlet,將創建一個新的實例。說白了就跟CGI一樣了,每次web請求都起一個進程來處理。
Servlet本身是規范,它需要實現了這組規范的Servlet容器來提供web能力。一提到servlet容器,條件反射的第一個想到的自然是Tomcat。Tomcat才是去實例化Servlet的那個他。而Tomcat里執行實例化Servlet的類叫StandardWrapper,它有個loadServlet的方法:
/** * Load and initialize an instance of this servlet, if there is not already * at least one initialized instance. This can be used, for example, to * load servlets that are marked in the deployment descriptor to be loaded * at server startup time. * @return the loaded Servlet instance * @throws ServletException for a Servlet load error */ public synchronized Servlet loadServlet() throws ServletException { // Nothing to do if we already have an instance or an instance pool if (!singleThreadModel && (instance != null)) return instance; PrintStream out = System.out; if (swallowOutput) { SystemLogHandler.startCapture(); } Servlet servlet; try { long t1=System.currentTimeMillis(); // Complain if no servlet class has been specified if (servletClass == null) { unavailable(null); throw new ServletException (sm.getString("standardWrapper.notClass", getName())); } InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager(); try { servlet = (Servlet) instanceManager.newInstance(servletClass); } catch (ClassCastException e) { unavailable(null); // Restore the context ClassLoader throw new ServletException (sm.getString("standardWrapper.notServlet", servletClass), e); } catch (Throwable e) { e = ExceptionUtils.unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(e); unavailable(null); // Added extra log statement for Bugzilla 36630: // https://bz.apache.org/bugzilla/show_bug.cgi?id=36630 if(log.isDebugEnabled()) { log.debug(sm.getString("standardWrapper.instantiate", servletClass), e); } // Restore the context ClassLoader throw new ServletException (sm.getString("standardWrapper.instantiate", servletClass), e); } if (multipartConfigElement == null) { MultipartConfig annotation = servlet.getClass().getAnnotation(MultipartConfig.class); if (annotation != null) { multipartConfigElement = new MultipartConfigElement(annotation); } } // Special handling for ContainerServlet instances // Note: The InstanceManager checks if the application is permitted // to load ContainerServlets if (servlet instanceof ContainerServlet) { ((ContainerServlet) servlet).setWrapper(this); } classLoadTime=(int) (System.currentTimeMillis() -t1); if (servlet instanceof SingleThreadModel) { if (instancePool == null) { instancePool = new Stack<>(); } singleThreadModel = true; } initServlet(servlet); fireContainerEvent("load", this); loadTime=System.currentTimeMillis() -t1; } finally { if (swallowOutput) { String log = SystemLogHandler.stopCapture(); if (log != null && log.length() > 0) { if (getServletContext() != null) { getServletContext().log(log); } else { out.println(log); } } } } return servlet; }
我們看到該方法是同步的(synchronized修飾符)。先看上面標黃的第一行,如果實現了之前說到的SigleThreadModel接口,那么這里的singleThreadModel就是true,就不會因為有Servlet實例而返回原有的Servlet了。但若反之,就返回原有的Servlet實例,符合單例的定義。
第二處標黃說明Tomcat是通過反射來實例化Servlet的。它先根據web.xml(或者起相同作用的@WebServlet)找到ServletClass的全路徑類名,然后通過類加載器得到Class對象,由Class對象取到構造器,通過構造器實例化ServletClass。
最后看下例子:
package com.wlf.demo.servlet; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * 一個servlet的demo */ @WebServlet(urlPatterns = {"/hello","/world"}) public class DemoServlet extends HttpServlet{ // 全局變量,多線程情況下將被改寫 private String globalVariable = ""; public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { // 后來的線程會覆蓋前面的線程的值 globalVariable = request.getParameter("param"); // 給界面點擊爭取時間 try{ Thread.sleep(5000); }catch (InterruptedException e) { e.printStackTrace(); } // 頁面輸出 PrintWriter pw = response.getWriter(); pw.println("<HTML>"); pw.println("<HEAD>"); pw.println("<title>Hello, world</title>"); pw.println("<body>"); pw.printf("<p>input: %s</p>", globalVariable); pw.println("</HEAD>"); pw.println("</HTML>"); } }
我們啟動tomcat,在瀏覽器打開兩個頁面分別訪問,然后我先刷新hello頁面,在它轉圈圈時去刷新world頁面,這時world的參數值就會覆蓋hello的參數值:
但如果我們給DemoServlet實現了SingleThreadModel:
public class DemoServlet extends HttpServlet implements SingleThreadModel
重新編譯、打包、部署、重啟tomcat后,重試上面的例子,並發問題就不存在了。當然,既然官方不建議你使用單線程模式,那么我們還是別用了,畢竟性能比起多線程來就低很多了。而多線程的問題我們可以通過線程封閉(全局變量改為局部變量)和加同步鎖(用到全局變量時增加synchronized代碼塊)等方法來解決。