Servlet是單例的嗎?


  如題,是嗎?首先我們得搞清楚啥是單例。一聊起單例,條件反射的第一個想到的自然是單例模式。單例模式的定義:一個類有且僅有一個實例,並且自行實例化向整個系統提供。如果按照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代碼塊)等方法來解決。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM