参考:https://blog.csdn.net/fredaq/article/details/9366043
一、概念
所谓Servlet容器其实说白了是符合Servlet规范的Java web容器。目前市场上面常用的开源Java Web容器有Tomcat,resin和Jetty。
注意Web容器和HTTP服务器的不同,虽然都是处理web请求的。
在HTTP服务器领域,Apache HTTP的效率是最高的,也是最稳定的,但是只能用于处理静态请求,如果需要支持动态页面请求,则必须要安装相应的插件,比如mod_perl可以处理Perl脚本,mod_python可以处理Python脚本。
上面列举的三种开源Web容器都是使用Java编写的Http服务器,也可以嵌入到Apache中使用。
Tomcat是使用最广的Java Web容器,功能强大,可扩展性强。最新版本的Tomcat(5.5.17)为了提高响应速度和效率,使用了Apache Portable Runtime(APR)作为最底层,使用了APR中包含Socket、缓冲池等多种技术,性能也提高了。APR也是Apache HTTPD的最底层。可想而知,同属于ASF(Apache Software Foundation)中的成员,互补互用的情况还是很多的,虽然使用了不同的开发语言。
二、Tomcat容器之多线程
Tomcat的线程池的实现在tomcat-util包中,包含了两种线程池方案。
方案1:使用APR的Pool技术,使用了JNI;
方案2:使用Java实现的ThreadPool
这里只介绍第二种
ThreadPool默认创建了5个线程,保存在一个200维的线程数组中,创建的时候启动这些线程,当没有请求到来的时候,这些线程均处于ready状态等待(其实就是一个while循环,一直在循环等待notify)。当请求到来时,空闲的线程就会被唤醒执行用户的请求。
具体的请求过程为:
- 服务启动的时候创建一个以为线程数组,长度为maxThread=200,并且创建空闲线程随时等待用户请求(minSpareThreads=5个)英语随时等待用户请求;
- 当用户请求到达时,调用threadpool.runIt(ThreadPoolRunnable)方法,讲一个需要执行的实例传给ThreadPool中。其中用户需要执行的实例必须实现ThreadPoolRunnable接口;
- ThreadPool首先查找空闲的线程,如果存在空闲线程,那么运行要执行ThreadPoolRunnable;如果不存在空闲线程且现存线程数量没有超过maxThreads,就一次性创建minSpareThreads个空闲线程;如果现存线程数量已经达到maxThreads,那么后来的请求就只能等待空闲线程释放了
- 如果得到空闲线程,则将该线程从线程数组中一走。接着唤醒已经找到的空闲线程,用它运行执行实例(ThreadPoolRunnable)。运行完ThreadPoolRunnable之后,九江该线程学会从新放到线程数组中,作为空闲线程供后续使用。
总结:Tomcat的线程池实现是比较简单的,ThreadPool.java也只有840行代码。用一个一维数组保存空闲的线程,每次以一个较小步伐(minSpareThreads5个)创建空闲线程并放到线程池中。使用时从数组中移走空闲的线程,用完后,再“归还”给线程池。
上面仅仅介绍了tomcat中线程池的实现,下面简要介绍tomcat线程池的使用
Tomcat有两种EndPoint,分别是AprEndpoint和PoolTcpEndpoint。前者自己实现了一套线程池(其实这和Tomcat 老版本的方案是相同的,至今Tomcat中还保留着老版本的线程池,PoolTcpEndpoint也有类似的代码,通过“策略”可以选择不同的线程池方案)。我们只关注PoolTcpEndpoint如何使用ThreadPool的。
首先,PoolTcpEndpoint创建了一个ThreadPoolRunnable实例——LeaderFollowerWorkerThread,实际上该实例就是接收(Accept)并处理(Process)用户socket请求。接着将该实例放进ThreadPool中并运行,此时就可以接收用户的请求了。当有Socket请求时,LeaderFollowerWorkerThread首先获得了Socket实例,注意此时LeaderFollowerWorkerThread并没有急着处理该Socket,而是在响应Socket消息前,再次将LeaderFollowerWorkerThread放进ThreadPool中,从而它(当然是另外一个线程了)可以继续处理其他用户的Socket请求;接着,拥有Socket的LeaderFollowerWorkerThread再来处理该用户的Socket请求。整个过程与传统的处理用户Socket请求是不同的,也和Tomcat老版本不同。传统的处理方法是:有一个后台运行的监听线程负责统一处理接收(注意只是“接收”)Socket请求,当有新的Socket请求时,将它赋值给一个Worker线程(通常是唤醒该线程),并有后者处理Socket请求,监听线程继续等待其他Socket请求。所以整个过程中有一个从Listener到Worker切换的过程。而新版本Tomcat很有创造性的使用了另外一种方法,正如前文所描述的,接收和处理某个用户Socket请求的始终是由一个线程全程负责,没有切换到其他线程处理,少了这种线程间的切换是否更有效率呢?我还不能确认。不过这种使用方式确实有别于传统模式,有种耳目一新的感觉。
线程池一般有三个重要参数:
1. 最大线程数。在程序运行的任何时候,线程数总数都不会超过这个数。如果请求数量超过最大数时,则会等待其他线程结束后再处理。2. 最大共享线程数,即最大空闲线程数。如果当前的空闲线程数超过该值,则多余的线程会被杀掉。3. 最小共享线程数,即最小空闲线程数。如果当前的空闲数小于该值,则一次性创建这个数量的空闲线程,所以它本身也是一个创建线程的步长。线程池有两个概念:1. Worker线程。工作线程主要是运行执行代码,有两种状态:空闲状态和运行状态。在空闲状态时,类似“休眠”,等待任务;处理运行状态时,表示正在运行任务(Runnable)。2. 辅助线程。主要负责监控线程池的状态:空闲线程是否超过最大空闲线程数或者小于最小空闲线程数等。如果不满足要求,就调整之。三、Servlet的单例多线程
上面介绍了tomcat中多线程的实现和工作机制,但是tomcat中的多线程仅仅用来接收请求,而处理处理请求并进行业务处理的是一个个Servlet,本小节中简要介绍Servlet如何处理多个请求的。
首先明确的是:Servlet容器默认是采用但实例多线程的方式来处理多个请求的:
- 当Web服务器启动的时候或者客户端发送请求到服务器的时候,Servlet就会被加载并且实例化(只实例化一次,所以同一个Servlet只存在一个实例);
- 容器初始化Servlet主要就是读取配置文件(例如tomcat通过server.xml文件中的<Connector>设置线程池中线程的数量,而初始化线程池通过web.xml文件初始化每个参数值等等)。
- 当请求到达时,Servlet容器通过调度线程(Dispatcher Thread)调度他管理下线程池中等待执行的线程(Worker Thread)给请求者;
- 线程根据请求的内容和web.xml文件找到相应的Servlet,并执行相应Servlet实例的service方法;
- 请求处理结束,Work Thread被放回鲜橙汁,等待再次被调用;
从上面可以看出:
- Servlet单实例,减少了产生servlet的开销;通过线程池来响应多个请求,提高了请求的响应时间;
- Servlet容器并不关心到达的Servlet请求访问的是否是同一个Servlet还是另一个Servlet,直接分配给它一个新的线程;
- 如果是同一个Servlet的多个请求,那么Servlet的service方法将在多线程中并发的执行;
- 每一个请求由ServletRequest对象来接受请求,由ServletResponse对象来响应该请求;
JSP中存在的多线程问题:
当客户端第一次请求某一个JSP文件时,服务端把该JSP编译成一个CLASS文件,并创建一个该类的实例,然后创建一个线程处理CLIENT端的请求。如果有多个客户端同时请求该JSP文件,则服务端会创建多个线程。每个客户端请求对应一个线程。以多线程方式执行可大大降低对系统的资源需求,提高系统的并发量及响应时间. 对JSP中可能用的的变量说明如下: 实例变量: 实例变量是在堆中分配的,并被属于该实例的所有线程共享,所以不是线程安全的. JSP系统提供的8个类变量 JSP中用到的OUT,REQUEST,RESPONSE,SESSION,CONFIG,PAGE,PAGECONXT是线程安全的(因为每个线程对应的request,respone对象都是不一样的,不存在共享问题), APPLICATION在整个系统内被使用,所以不是线程安全的. 局部变量: 局部变量在堆栈中分配,因为每个线程都有它自己的堆栈空间,所以是线程安全的. 静态类: 静态类不用被实例化,就可直接使用,也不是线程安全的. 外部资源: 在程序中可能会有多个线程或进程同时操作同一个资源(如:多个线程或进程同时对一个文件进行写操作).此时也要注意同步问题. 使它以单线程方式执行,这时,仍然只有一个实例,所有客户端的请求以串行方式执行。这样会降低系统的性能 问题 问题一. 说明其Servlet容器如何采用单实例多线程的方式来处理请求 问题二. 如何在开发中保证servlet是单实例多线程的方式来工作(也就是说如何开发线程安全的servelt)。 一. Servlet容器如何同时来处理多个请求 Java的内存模型JMM(Java Memory Model) JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有实例变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存由缓存和堆栈两部分组成,缓存中保存的是主存中变量的拷贝,缓存可能并不总和主存同步,也就是缓存中变量的修改可能没有立刻写到主存中;堆栈中保存的是线程的局部变量,线程之间无法相互直接访问堆栈中的变量。根据JMM,我们可以将论文中所讨论的Servlet实例的内存模型抽象为图所示的模型。 工作者线程Work Thread:执行代码的一组线程。 调度线程Dispatcher Thread:每个线程都具有分配给它的线程优先级,线程是根据优先级调度执行的。 Servlet采用多线程来处理多个请求同时访问。servlet依赖于一个线程池来服务请求。线程池实际上是一系列的工作者线程集合。Servlet使用一个调度线程来管理工作者线程。 当容器收到一个Servlet请求,调度线程从线程池中选出一个工作者线程,将请求传递给该工作者线程,然后由该线程来执行Servlet的service方法。当这个线程正在执行的时候,容器收到另外一个请求,调度线程同样从线程池中选出另一个工作者线程来服务新的请求,容器并不关心这个请求是否访问的是同一个Servlet.当容器同时收到对同一个Servlet的多个请求的时候,那么这个Servlet的service()方法将在多线程中并发执行。 Servlet容器默认采用单实例多线程的方式来处理请求,这样减少产生Servlet实例的开销,提升了对请求的响应时间,对于Tomcat可以在server.xml中通过元素设置线程池中线程的数目。 就实现来说: 调度者线程类所担负的责任如其名字,该类的责任是调度线程,只需要利用自己的属性完成自己的责任。所以该类是承担了责任的,并且该类的责任又集中到唯一的单体对象中。而其他对象又依赖于该特定对象所承担的责任,我们就需要得到该特定对象。那该类就是一个单例模式的实现了。 注意:服务器可以使用多个实例来处理请求,代替单个实例的请求排队带来的效益问题。服务器创建一个Servlet类的多个Servlet实例组成的实例池,对于每个请求分配Servlet实例进行响应处理,之后放回到实例池中等待下此请求。这样就造成并发访问的问题。 此时,局部变量(字段)也是安全的,但对于全局变量和共享数据是不安全的,需要进行同步处理。而对于这样多实例的情况SingleThreadModel接口并不能解决并发访问问题。 SingleThreadModel接口在servlet规范中已经被废弃了。
上面说了Servlet在容器中是单实例,因此要注意线程安全问题,尽量不要再servlet里面保存状态值,但是并不是不可以保存。如果必须要这么做那么就必须要考虑线程安全问题了,通常有两种做法:
1、实现SingleThreadModel接口
该接口指定了系统如何处理对同一个Servlet的调用。如果一个Servlet被这个接口指定,那么在这个Servlet中的service方法将不会有两个线程被同时执行,当然也就不存在线程安全的问题。这种方法只要将前面的Concurrent Test类的类头定义更改为: Public class Concurrent Test extends HttpServlet implements SingleThreadModel { ………… }
2、同步对共享数据的操作
使用synchronized 关键字能保证一次只有一个线程可以访问被保护的区段,在本论文中的Servlet可以通过同步块操作来保证线程的安全。同步后的代码如下: ………… Public class Concurrent Test extends HttpServlet { ………… Username = request.getParameter ("username"); Synchronized (this){ Output = response.getWriter (); Try { Thread. Sleep (5000); } Catch (Interrupted Exception e){} output.println("用户名:"+Username+""); } } }
虽然不提倡将Servlet做成线程安全的,但是可能出于某种原因你必须这么做,那么你就要清楚这么做的代价有哪些:
如果一个Servlet实现了SingleThreadModel接口,那么Servlet引擎将会为每一个请求创建一个单独的Servlet实例,在请求结束之后销毁,这将会造成大量的系统开销。SingleThreadModel在Servlet2.4中已经不再提倡使用;同样在程序中使用同步来保护要使用的共享数据,也会使系统的性能大大下降。这是应为被同步的代码块在同一个时刻只能有一个线程执行他,使得其同时处理客户请求的吞吐量降低,而且很多客户处于阻塞状态。另外保证主存内容和线程中的工作内存中的数据的一致性,要频繁的刷新缓存,这也会大大影响系统的性能。所以在实际的开发中也应该避免或者最小化Servlet中的同步代码;在Servlet中避免使用实例变量是保证Servlet线程安全的最佳选择。从Java内存模型也可以知道,方法中的临时变量是在栈上分配空间,而且每一个线程都有自己私有的栈空间,所以他们不会影响线程的安全。
四、线程安全的Servlet
1,变量的线程安全:这里的变量指字段和共享数据(如表单参数值)。 a,将 参数变量 本地化。多线程并不共享局部变量.所以我们要尽可能的在servlet中使用局部变量。 例如:String user = ""; user = request.getParameter("user"); b,使用同步块Synchronized,防止可能异步调用的代码块。这意味着线程需要排队处理。在使用同板块的时候要尽可能的缩小同步代码的范围,不要直接在sevice方法和响应方法上使用同步,这样会严重影响性能。 2,属性的线程安全:ServletContext,HttpSession,ServletRequest对象中属性。 ServletContext:(线程是不安全的) ServletContext是可以多线程同时读/写属性的,线程是不安全的。要对属性的读写进行同步处理或者进行深度Clone()。所以在Servlet上下文中尽可能少量保存会被修改(写)的数据,可以采取其他方式在多个Servlet中共享,比方我们可以使用单例模式来处理共享数据。 HttpSession:(线程是不安全的) HttpSession对象在用户会话期间存在,只能在处理属于同一个Session的请求的线程中被访问,因此Session对象的属性访问理论上是线程安全的。 当用户打开多个同属于一个进程的浏览器窗口,在这些窗口的访问属于同一个Session,会出现多次请求,需要多个工作线程来处理请求,可能造成同时多线程读写属性。这时我们需要对属性的读写进行同步处理:使用同步块Synchronized和使用读/写器来解决。 ServletRequest:(线程是安全的) 对于每一个请求,由一个工作线程来执行,都会创建有一个新的ServletRequest对象,所以ServletRequest对象只能在一个线程中被访问。ServletRequest是线程安全的。注意:ServletRequest对象在service方法的范围内是有效的,不要试图在service方法结束后仍然保存请求对象的引用。 4,不要在Servlet中创建自己的线程来完成某个功能。 Servlet本身就是多线程的,在Servlet中再创建线程,将导致执行情况复杂化,出现多线程安全问题。 5,在多个servlet中对外部对象(比方文件)进行修改操作一定要加锁,做到互斥的访问。 6,javax.servlet.SingleThreadModel接口是一个标识接口,如果一个Servlet实现了这个接口,那Servlet容器将保证在一个时刻仅有一个线程可以在给定的servlet实例的service方法中执行。将其他所有请求进行排队。 PS:Servlet并非只是单例的. 当container开始启动,或是客户端发出请求服务时,Container会按照容器的配置负责加载和实例化一个Servlet(也可以配置为多个,不过一般不这么干).不过一般来说一个servlet只会有一个实例。1) Struts2的Action是原型,非单实例的;会对每一个请求,产生一个Action的实例来处理。 2) Struts1的Action,Spring的Ioc容器管理的bean 默认是单实例的. Struts1 Action是单实例的,spring mvc的controller也是如此。因此开发时要求必须是线程安全的,因为仅有Action的一个实例来处理所有的请求。单例策略限制了Struts1 Action能作的事,并且要在开发时特别小心。Action资源必须是线程安全的或同步的。 Spring的Ioc容器管理的bean 默认是单实例的。Struts2 Action对象为每一个请求产生一个实例,因此没有线程安全问题。(实际上,servlet容器给每个请求产生许多可丢弃的对象,并且不会导致性能和垃圾回收问题)。当Spring管理Struts2的Action时,bean默认是单实例的,可以通过配置参数将其设置为原型。(scope="prototype )
五、Servlet生命周期
1.Servlet在web服务器启动时被加载并实例化,容器运行其init方法初始化,请求到达时运行其service方法;
2.service运行请求对应的doXXX(doGet,doPost)方法;
3.服务器销毁实例,运行其destory方法; Servlet的生命周期由Servlet容器管理;
三个概念的理解: Servlet容器 & Web容器 & 应用服务器
1、Servlet容器的主要任务就是管理Servlet的生命周期;
2、Web容器也称之为web服务器,主要任务就是管理和部署web应用的;
3、应用服务器的功能非常强大,不仅可以管理和部署web应用,也可以部署EJB应用,实现容器管理的事务等等 Web服务器/Web容器就是跟基于HTTP的请求打交道,而EJB容器更多是跟数据库,邮件服务、事务管理等服务接口交互,所以应用服务器的功能是很多的。
常见的web服务器就是Tomcat,但Tomcat同样也是Servlet服务器; 常见的应用服务器有WebLogic,WebSphere,但都是收费的; 没有Servlet容器,可以用Web容器直接访问静态Html页面,比如安装了apache等;如果需要显示Jsp/Servlet,就需要安装一个Servlet容器;但是光有servlet容器也是不够的,它需要被解析为html显示,所以仍需要一个web容器;所以,我们常把web容器和Servlet容器视为一体,因为他们两个容器都有对方的功能实现了,都没有独立的存在了,比如tomcat!
六、java Web容器工作原理
以tomcat为例进行说明,here we go。先上两张tomcat结构图
一个Tomcat web容器启动server服务器从而对外提供service服务,图一中一个service瓦片代表一个部署在tomcat上面的web服务,一个Service的核心组件为Container,而一个Container的核心是Servlet容器,而Servlet容器中管理Servlet的是Context容器;因此就形成了这样的一种组件依赖链:
Tomcat---Service---Container---Servlet容器---Context容器
Servlet容器的启动过程也是这种顺序,如下。
Web应用有Tomcat实例添加到Tomcat 中,即由Tomcat管理一个新添加的Context容器。前面已经提到一个web应用对应一个Context容器,也就是Servlet运行时的Servlet容器。在Servlet API中有一个ServletContextListener接口,它能够监听ServletContext对象的生命周期,实际上就是监听Web应用的生命周期。
当Web应用启动或者终止的时候,会触发ServletContextEvent事件,该时间是有ServletContextListener来处理。在ServletContextListner接口中定义了处理ServletContextEvent时间的两个方法。contextInitialized(ServletContextEvent event)和contextDestryed(ServletContextEvent event)方法,在Servlet容器启动Web应用时和终止Web应用时调用。
- Servlet容器启动时,触发servletContextEvent事件,并通知相应的监听器servletContextListener;
- Servlet容器在启动的过程中通过servletContextListener监视servletContext的状态(初始化或者销毁servletContext);即
- servletContextListener中通过contextInitialized初始化方法,根据web.xml对servletContext进行配置,即将Context容器的属性缓存在内存中,供Service服务利用;
contextConfig是一个监听器,在Tomcat创建Context容器时被加入到servletContext中,contextConfig负责整个Web应用的配置文件(包括web.xml)的解析工作。
Web应用的初始化是在contextConfig中实现的,应用的初始化主要是对Web.xml配置文件进行解析,这个文件中描述了Web应用的关键信息,同时也是整个Web应用的入口。Tomcat将web.xml文件解析之后将其中的属性设置到Context容器中,主要包涵下面动作:
- 启动一个WEB项目的时候,WEB容器会去读取它的配置文件web.xml,读取<context-param>结点。
- 容创建一个ServletContext(servlet上下文),这个 web项目的所有部分都将共享这个上下文。
- 容器将<context-param>转换为键值对,并交给 servletContext。 因为listener, filter 等在初始化时会用到这些上下文中的信息,所以要先加载。
- 容器创建<listener>中的类实例,创建监听器。
- 加载filter和servlet
load- on-startup 元素在web应用启动的时候指定了servlet被加载的顺序,它的值必须是一个整数。
如果它的值是一个负整数或是这个元素不存在,那么容器会在该servlet被调用的时候,加载这个servlet。如果值是正整数或零,容器在配置的时候就加载并初始化这个servlet,容器必须保证值小的先被加载。如果值相等,容器可以自动选择先加载谁。
web.xml 的加载顺序是:context-param -> listener -> filter -> servlet。