Servlet 3.1規范學習筆記(一)——Servlet生命周期和線程安全性問題

jopen 8年前發布 | 15K 次閱讀 Servlet 線程 Java開發

每一個Java程序員都知道Servlet的重要性,在Java Web 開發中,程序員一定避不開 Servlet,很多 Java Web 框架都是基于 Servlet 來構建的,最終開發完成的應用也一定要在Servlet 容器里運行。Java 程序員應該都知道,Servlet API 提供了一套底層的 API 來處理 HTTP 請求和響應。所以,了解Servlet的API以及規范對于Java開發人員來說至關重要。

本文內容主要是作者在學習 Servlet3.1 規范的過程中記錄的學習筆記,并且有些在規范中解釋的并不是很清楚的地方作者參考了其他資料進行了總結。

什么是Servlet

Servlet(Server Applet) 是基于 Java 技術的 web 組件,該組件由容器托管,用于生成動態內容。他是用Java編寫的服務器端程序。其主要功能在于交互式地瀏覽和修改數據,生成動態Web內容。

什么是 Servlet 容器

Servlet容器是web server或application server的一部分,供基于請求/響應發送模型的網絡服務,解碼基于 MIME 的請求,并且格式化基于 MIME 的響應。Servlet 容器也包含并管理 Servlet 生命周期。

所有 Servlet 容器必須支持基于 HTTP 協議的請求/響應模型,比如像基于 HTTPS(HTTP over SSL)協議的 請求/應答模型可以選擇性的支持。容器必須實現的 HTTP 協議版本包含 HTTP/1.0 和 HTTP/1.1。

Servlet 生命周期

Servlet 通過一個定義良好的生命周期來進行管理,該生命周期規定了 Servlet 如何被加載、實例化、初始化、 處理客戶端請求,以及何時結束服務。該生命周期可以通過 javax.servlet.Servlet 接口中的 init 、 service 和 destroy 這些 API 來表示,所有 Servlet 必須直接或間接的實現 GenericServlet 或 HttpServlet 抽象類。

Servlet的生命周期有四個階段:加載并實例化、初始化、請求處理、銷毀。主要涉及到的方法有 init 、 service 、 doGet 、 doPost 、 destory 等

加載并實例化

Servlet容器負責加載和實例化Servelt。當Servlet容器啟動時,或者在容器檢測到需要這個Servlet來響應第一個請求時,創建Servlet實例。當Servlet容器啟動后,Servlet通過類加載器來加載Servlet類,加載完成后再new一個Servlet對象來完成實例化。

初始化

在Servlet實例化之后,容器將調用 init() 方法,并傳遞實現ServletConfig接口的對象。在 init() 方法中,Servlet可以部署描述符中讀取配置參數,或者執行任何其他一次性活動。在Servlet的整個生命周期類, init() 方法只被調用一次。

請求處理

當Servlet初始化后,容器就可以準備處理客戶機請求了。當容器收到對這一Servlet的請求,就調用Servlet的 service() 方法,并把請求和響應對象作為參數傳遞。當并行的請求到來時,多個service()方法能夠同時運行在獨立的線程中。 service() 方法檢查 HTTP 請求類型(GET、POST、PUT、DELETE 等),并在適當的時候調用 doGet() 、 doPost() 、 doPut() , doDelete() 等方法。

一般情況下,當開發基于 HTTP 協議的 Servlet 時,Servlet 開發人員只關注自己的 doGet 和 doPost 請求處理 方法即可。其他方法被認為是非常熟悉 HTTP 編程的程序員使用的方法。

銷毀

一旦Servlet容器檢測到一個Servlet要被卸載,這可能是因為要回收資源或者因為它正在被關閉,容器會在所有Servlet的 service() 線程之后,調用Servlet的 destroy() 方法。然后,Servlet就可以進行無用存儲單元收集清理。這樣Servlet對象就被銷毀了。這四個階段共同決定了Servlet的生命周期。

Servlet的線程安全問題

為了有效利用JVM允許多個線程訪問同一個實例的特性,來提高服務器性能。在非分布式系統中,Servlet容器只會維護一個Servlet的實例。

如果 Web 應用中的 Servlet 被標注為分布式的,容器應該為每一個分布式應用程序的 JVM 維護一個 Servlet 實例池。

Servlet容器通過維護一個線程池來處理多個請求,線程池中維護的是一組工作者線程(Worker Thread)。Servlet容器通過一個調度線程(Dispatcher Thread)來調度線程池中的線程。

當客戶端的servlet請求到來時,調度線程會從線程池中選出一個工作者線程并將請求傳遞給該線程,該線程就會執行對應servlet實例的service方法。同樣,當客戶端發起另一個servlet請求時,調度線程會從線程池中選出另一個線程去執行servlet實例的service方法。Servlet容器并不關心這些線程訪問的是同一個servlet還是不同的servlet,當多個線程訪問同一個servlet時,該servlet實例的service方法將在多個線性中并發執行。

所以,Servlet對象是單實例多線程,Servlet不是線程安全的

為什么不安全?

先看兩個定義:

實例變量:實例變量在類中定義。類的每一個實例都擁有自己的實例變量,如果多個線程同時訪問該實例的方法,而該方法又使用到實例變量,那么這些線程同時訪問的是同一個實例變量,會共享該實例變量。

局部變量:局部變量在方法中定義。每當一個線程訪問局部變量所在的方法時,在線程的堆棧中就會創建這個局部變量,線程執行完這個方法時,該局部變量就被銷毀。所有多個線程同時訪問該方法時,每個線程都有自己的局部變量,不會共享。

看如下代碼:

public class MyServlet extends HttpServlet{
 private static final long serialVersionUID = 1L;

 private String userName1 = null;//實例變量

 @Override
 protected void doGet(HttpServletRequest req, HttpServletResponse resp)
         throws ServletException, IOException{
  userName1 = req.getParameter("userName1");

  String userName2 = req.getParameter("userName2");//局部變量

  //TODO 其他處理
 }
}

userName1則是共享變量,多個線程會同時訪問該變量,是線程不安全的。

userName2是局部變量,不管多少個線程同時訪問,都是線程安全的。

解決Servlet的線程安全問題

如果不涉及到全局共享變量,就直接使用局部變量

如果使用到全局共享的場景,可以使用加鎖的方式.對全局變量的讀寫操作置于synchronized同步塊中,這樣不同線程排隊依次執行該代碼塊,從而避免線程不安全情況發生。還可以使用線程安全的數據類型。比如hashtable,blockQueue等

參考資料:

servlet和Jsp生命周期解讀

Servlet的線程安全問題

java servlet拾遺(3)-servlet 線程安全問題

來自: http://www.hollischuang.com/archives/849

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!