【tomcat】08 Server組件與Service組件

一、介紹

1、Server組件和Service組件是Tomcat核心組件中最外層級的兩個組件,Server組件可以看成Tomcat的運行實例的抽象,而Service組件則可以看成Tomcat內的不同服務的抽象。

2、Server組件包含若干Listener組件、GlobalNamingResources組件及若干Service組件。

3、Service組件則包含若干Connector組件和Executor組件。

二、Server 組件

1、介紹

  1. Server組件是代表整個Tomcat的Servlet容器,從server.xml配置文件也可以看出它屬於最外層組件。
  1. 默認配置了6個監聽器組件,每個監聽器負責各自的監聽任務處理。
  1. GlobalNamingResources組件通過JNDI提供統一的命名對象訪問接口,它的使用範圍是整個Server。ServerSocket組件監聽某個端口是否有SHUTDOWN命令,一旦接收到則關閉Server,即關閉Tomcat。

2、Server組件的作用

  1. 提供了監聽機制,用於在Tomact 整個生命週期對不同事件進行處理
  1. 提供了Tomcat 容器全局的命名資源實現
  1. 監聽某個端口以接收全局的命名資源實現

3、生命週期監聽

  1. 初始化前、初始化中、初始化後、啓動前、啓動中、啓動後、停止前、停止中、停止後、銷燬中、銷燬後等。爲了在Server組件的某階段執行某些邏輯,於是提供了監聽器機制。
  1. 在Tomcat中實現一個生命週期監聽器很簡單,只要實現LifecycleListener接口即可,在lifecycleEvent方法中對感興趣的生命週期事件進行處理。

1) AprLifecycleListener監聽

a: Tomcat會使用APR本地庫進行優化,通過JNI方式調用本地庫能大幅提高對靜態文件的處理能力。

b: AprLifecycleListener監聽器對初始化前的事件和銷燬後的事件感興趣,在Tomcat初始化前,該監聽器會嘗試初始化APR庫,假如能初始化成功,則會使用APR接受客戶端的請求並處理請求。

c: 在Tomcat銷燬後,該監聽器會做APR的清理工作。

  1. JasperListener監聽

在Tomcat初始化前該監聽器會初始化Jasper組件,Jasper是Tomcat的JSP編譯器核心引擎,用於在Web應用啓動前初始化Jasper。

  1. JreMemoryLeakPreventionListener監聽

a:該監聽器主要提供解決JRE內存泄漏和鎖文件的一種措施,該監聽器會在Tomcat初始化時使用系統類加載器先加載一些類和設置緩存屬性,以避免內存泄漏和鎖文件。

b: JRE內存泄漏問題。內存泄漏的根本原因在於當垃圾回收器要回收時無法回收本該被回收的對象。假如一個待回收對象被另外一個生命週期很長的對象引用,那麼這個對象將無法被回收。

3.1) JRE內存泄漏是因爲上下文類加載器導致的內存泄漏。

a: 在JRE庫中某些類在運行時會以單例對象的形式存在,並且它們會存在很長一段時間,基本上是從Java程序啓動到關閉。

b: JRE庫的這些類使用上下文類加載器進行加載,並且保留了上下文類加載器的引用,所以將導致被引用的類加載器無法被回收,而Tomcat在重加載一個Web應用時正是通過實例化一個新的類加載器來實現的,舊的類加載器無法被垃圾回收器回收,導致內存泄漏。

c: 某上下文類加載器爲WebappClassloader的線程加載JRE的DriverManager類,此過程將導致WebappClassloader被引用,後面該WebappClassloader將無法被回收,發生內存泄漏。

3.2) JRE內存泄漏是因爲線程啓動另外一個線程並且新線程無止境地執行。

a) 在JRE庫中存在某些類,當線程加載它時,它會創建一個新線程並且執行無限循環,新線程的上下文類加載器會繼承父線程的上下文類加載器,所以新線程包含了上下文類加載器的應用,導致該類加載器無法被回收,最終導致內存泄漏。

b) 某上下文類加載器爲Webappclassloader的線程加載JRE的Disposer類,此時該線程會創建一個新的線程,新線程的上下文類加載器爲Webappclassloader,隨後新線程將進入一個無限循環的執行中,最終該Webappclassloader將無法被回收,發生內存泄漏。

3.2) JRE內存泄漏與線程的上下文類加載器有很大的關係。

a: 解決JRE內存泄漏,嘗試讓系統類加載器加載這些特殊的JRE庫類。Tomcat中即使用了JreMemoryLeakPreventionListener監聽器來做這些事
b: 代碼

ClassLoader loader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
DriverManager.getDrivers();
try {
    Class.forName("sun.java2d.Disposer");
} catch (ClassNotFoundException cnfe) {
    Thread.currentThread().setContextClassLoader(loader);
}

3.3) 在Tomcat啓動時,先將當前線程的上下文類加載器設置爲系統類加載器,再執行DriverManager.getDrivers()和Class.forName(“sun.java2d.Disposer”),會加載這些類,此時的線程上下文爲系統類加載器,加載完這些特殊的類後再將上下文類加載器還原。此時,如果Web應用使用到這些類,由於它們已經加載到系統類加載器中,因此重啓Web應用時不會存在內存泄漏。

3.4) JRE還有其他類也存在內存泄漏的可能,如javax.imageio.ImageIO、java.awt.Toolkit、sun.misc.GC、javax.security.auth.Policy、javax.security.auth.login.Configuration、java.security.Security、javax.xml.parsers.DocumentBuilderFactory、com.sun.jndi.ldap.LdapPoolManager等

3.5) 鎖文件的情景主要由URLConnection默認的緩存機制導致,在Windows系統下當使用URLConnection的方式讀取本地Jar包裏面的資源時,它會將資源內存緩存起來,這就導致了該Jar包被鎖。此時,如果進行重新部署將會失敗,因爲被鎖的文件無法刪除。爲了解決鎖文件問題,可以將URLConnection設置成默認不緩存,而這個工作也交由JreMemoryLeakPreventionListener完成。

在Tomcat啓動時,實例化一個URLConnection,然後通過setDefaultUseCaches(false)設置成默認不緩存,這樣後面使用URLConnection將不會因爲緩存而鎖文件。JreMemoryLeakPreventionListener監聽器完成上面的工作即能避免JRE內存泄漏。

  1. GlobalResourcesLifecycleListener監聽

主要負責實例化Server組件裏面JNDI資源的MBean,並提交由JMX管理。監聽器對生命週期內的啓動事件和停止事件感興趣,它會在啓動時爲JNDI創建MBean,而在停止時銷燬MBean。

  1. ThreadLocalLeakPreventionListener監聽
  1. 主要解決ThreadLocal的使用可能帶來的內存泄漏問題。監聽器會在Tomcat啓動後將監聽Web應用重加載的監聽器註冊到每個Web應用上,當Web應用重加載時,該監聽器會將所有工作線程銷燬並再創建,以避免ThreadLocal引起內存泄漏。
  1. ThreadLocal引起的內存泄漏問題的根本原因也在於當垃圾回收器要回收時無法回收,因爲使用了ThreadLocal的對象被一個運行很長時間的線程引用,導致該對象無法被回收。
  1. ThreadLocal導致內存泄漏的經典場景是Web應用重加載,當Tomcat啓動後,對客戶端的請求處理都由專門的工作線程池負責。線程池中線程的生命週期一般都會比較長,假如Web應用中使用了ThreadLocal保存AA對象,而且AA類由Webappclassloader加載,那麼它就可以看成線程引用了AA對象。Web應用重加載是通過重新實例化一個Webappclassloader類加載器來實現的,由於線程一直未銷燬,舊的Webappclassloader也無法被回收,導致了內存泄漏。
  1. 解決ThreadLocal內存泄漏最徹底的方法就是當Web應用重加載時,把線程池內的所有線程銷燬並重新創建,這樣就不會發生線程引用某些對象的問題了。Tomcat中處理ThreadLocal內存泄漏的工作其實主要就是銷燬線程池原來的線程,然後創建新線程。分兩步,第一步先將任務隊列堵住,不讓新任務進來;第二步將線程池中所有線程停止。
  1. ThreadLocalLeakPreventionListener監聽器的工作就是實現當Web應用重加載時銷燬線程池的線程並重新創建新線程,以此避免ThreadLocal內存泄漏。
  1. NamingContextListener監聽

主要負責Server組件內全局命名資源在不同生命週期的不同操作,在Tomcat啓動時創建命名資源、綁定命名資源,在Tomcat停止前解綁命名資源、反註冊MBean。

4、全局命名資源

  1. Server組件包含了一個全局命名資源,它提供的命名對象通過ResourceLink可以給所有Web應用使用。
  1. ContextResources、ContextEjb、 ContextEnvironment、ContextLocalEjb、MessageDestinationRef
    ContextResourceEnvRef、ContextResourceEnvRef、 ContextResourceLink、ContextService
  1. Tomcat啓動時將server.xml配置文件裏面的GlobalNamingResources節點通過Digester框架映射到一個NamingResources對象。這個對象裏面包含了不同類型的資源對象,同時會創建一個NamingContextListener監聽器,這個監聽器負責在Tomcat初始化啓動期間完成對命名資源的所有創建、組織、綁定等工作,使之符合JNDI標準。而創建、組織、綁定等是根據NamingResources對象描述的資源屬性進行處理的,綁定的路徑由配置文件的Resource節點的name屬性決定,name即爲JNDI對象樹的分支節點,例如,name爲“jdbc/myDB”,那麼此對象就可通過“java:jdbc/myDB”訪問,而樹的位置應該是jdbc/myDB,但在Web應用中是無法直接訪問全局命名資源的。因爲要訪問全局命名資源,所以資源都必須放在Server組件中。

5、監聽SHUTDOWN命令

  1. Server會另外開放一個端口用於監聽關閉命令,這個端口默認爲8005,此端口與接收客戶端請求的端口並非同一個。客戶端傳輸的第一行如果能匹配關閉命令(默認爲SHUTDOWN),則整個Server將會關閉
  2. Tomcat中有兩類線程,一類是主線程,另外一類是daemon線程。當Tomcat啓動時,Server將被主線程執行,其實就是完成所有的啓動工作,包括啓動接收客戶端和處理客戶端報文的線程,這些線程都是daemon線程。所有啓動工作完成後,主線程將進入等待SHUTDOWN命令的環節,它將不斷嘗試讀取客戶端發送過來的消息,一旦匹配SHUTDOWN命令則跳出循環。主線程繼續往下執行Tomcat的關閉工作。最後主線程結束,整個Tomcat停止。
  1. 監聽SHUTDOWN命令簡單瞭解:打開本地8005端口監聽客戶端,一旦有客戶端連接,就嘗試讀取客戶端的命令,如果客戶端發送的命令爲SHUTDOWN,則跳出循環,讓整個主線程執行完畢,也就意味着程序執行完關閉。假如輸入的命令並非爲SHUTDOWN,則進去下一個循環,等待下一個客戶端的連接。

三、Service 組件

1、介紹

  1. Service組件是若干Connector組件和Executor組件組合而成的概念。Connector組件負責監聽某端口的客戶端請求,不同的端口對應不同的Connector。Executor組件在Service抽象層面提供了線程池,讓Service下的組件可以共用線程池。默認情況下,不同的Connector組件會自己創建線程池來使用,而通過Service組件下的Executor組件則可以實現線程池共享,每個Connector組件都使用Service組件下的線程池。除了Connector組件之外,其他的組件也可以使用。

2、Tomcat中線程池的實現

  1. “池”的引入是爲了在某些場景下提高系統某些關鍵節點的性能和效率,最典型的例子就是數據庫連接池。數據庫連接的建立和銷燬都是很耗時耗資源的操作。爲了查詢數據庫中某條記錄,最原始的一個過程是建立連接,發送查詢語句,返回查詢結果,銷燬連接。假如僅僅是一個很簡單的查詢語句,那麼建立連接與銷燬連接兩個步驟就已經佔所有時間消耗的絕大部分,效率顯然讓人無法接受。於是想到儘可能減少創建和銷燬連接操作,連接相對於查詢是無狀態的,不必每次查詢都重新生成和銷燬連接,可以維護這些通道維護以供下一次查詢或其他操作使用。維護這些管道的工作就交給了“池”。
  1. 線程池也是類似於數據庫連接池的一種池。線程是爲多任務而引入的概念,每個線程在任意時刻執行一個任務,假如多個任務要併發執行,則要用到多線程技術。每個線程都有自己的生命週期,以創建爲始,以銷燬爲末。線程的運行階段佔整個生命週期的比重不同。引入了線程池,它的核心思想就是把運行階段儘量拉長,對於每個任務的到來,不是重複建立、銷燬線程,而是重複利用之前建立的線程執行任務。

3、自己實現一個線程池

  1. 思路在系統啓動時建立一定數量的線程並做好線程維護工作,一旦有任務到來即從線程池中取出一條空閒的線程執行任務。線程池的屬性包含初始化線程數量、線程數組、任務隊列。

a: 初始化線程數量指線程池初始化的線程數,線程數組保存了線程池中的所有線程,任務隊列指添加到線程池中等待處理的所有任務。

b: 線程池裏有多個個線程,池裏線程的工作就是不斷循環檢測任務隊列中是否有需要執行的任務,如果有,則處理並移出任務隊列。可以說線程池中的所有線程的任務就是不斷檢測任務隊列並不斷執行隊列中的任務。

  1. 使用線程池時只須實例化一個對象,構造函數就會創建相應數量的線程並啓動線程,啓動的線程無限循環地檢測任務隊列,執行方法execute()僅僅把任務添加到任務隊列中。需要注意的一點是,所有任務都必須實現Runnable接口,這是線程池的任務隊列與工作線程的約定,JUC工具包作者DougLea當時如此規定,工作線程檢測任務隊列並調用隊列的run()方法,假如重新寫一個線程池,就完全可以自己定義一個不一樣的任務接口。一個完善的線程池並不像下面的例子那樣簡單,它需要提供啓動、銷燬、增加工作線程的策略,最大工作線程數,各種狀態的獲取等操作,而且工作線程也不可能始終做無用循環,需要對任務隊列使用wait、notify優化,或者將任務隊列改用爲阻塞隊列。
package com.example.tomcat.http;


import java.util.LinkedList;
import java.util.List;

/**
 * @author haoxiansheng
 */
public final class ThreadPool {
    private final int worker_num;

    private WorkerThread[] workerThreads;

    private List<Runnable> taskQueue = new LinkedList<>();

    private static ThreadPool threadPool;

    public ThreadPool(int worker_num) {
        this.worker_num = worker_num;
        workerThreads = new WorkerThread[worker_num];
        for (int i = 0; i < worker_num; i++) {
            workerThreads[i] = new WorkerThread();
            workerThreads[i].start();
        }
    }

    public void execute(Runnable task) {
        synchronized (taskQueue) {
            taskQueue.add(task);
        }
    }

    private class WorkerThread extends Thread {
        @Override
        public void run() {
            Runnable runnable = null;
            while (true) {
                synchronized (taskQueue) {
                    if (!taskQueue.isEmpty()) {
                        runnable = taskQueue.remove(0);
                        runnable.run();
                    }
                }
            }
        }
    }
}

  1. 自己實現線程池處理很容易產生死鎖問題,同時線程池內的狀態同步操作不當也可能導致意想不到的問題。除此之外,還有很多其他的併發問題,除非是很有併發的經驗才能儘可能減少可能的錯誤。
  1. 直接使用JDK的JUC工具包即可,它是由DougLea編寫的優秀併發程序工具,僅線程池就已經提供了好多種類的線程池,實際開發中可以根據需求選擇合適的線程池。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章