什么是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 接口中的 initservice 和 destroy 这些 API 来表示,所有 Servlet 必须直接或间接的实现 GenericServlet 或 HttpServlet 抽象类。

Servlet的生命周期有四个阶段:加载并实例化、初始化、请求处理、销毁。主要涉及到的方法有initservicedoGetdoPostdestory

加载并实例化

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是单例还是多例?


在Servlet规范中,对于Servlet单例与多例定义如下:

“Deployment Descriptor”, controls how the servlet container provides instances of the servlet.For a servlet not hosted in a distributed environment (the default), the servlet container must use only one instance per servlet declaration. However, for a servlet implementing the SingleThreadModel interface, the servlet container may instantiate multiple instances to handle a heavy request load and serialize requests to a particular instance.

上面规范提到,
如果一个Servlet没有被部署在分布式的环境中,一般web.xml中声明的一个Servlet只对应一个实例。

而如果一个Servlet实现了SingleThreadModel接口,就会被初始化多个实例。实例有多少呢,这里没细说。

下面再从Tomcat的源码中找寻下具体的参考实现是什么样子的。以下代码来源于Tomcat的StandardWrapper类。我把其中不太相关的部分做了删除。

public Servlet allocate() throws ServletException {
boolean newInstance = false;
if (!singleThreadModel) {
// Load and initialize our instance if necessary
if (instance == null) {
synchronized (this) {
if (instance == null) {
try {
instance = loadServlet();
} catch (ServletException e) {}}}}
if (singleThreadModel) {
if (newInstance) {
synchronized (instancePool) {
instancePool.push(instance); //如果实现STM接口,就放到一个栈里
nInstances++;
}}
} else {
if (!newInstance) {
countAllocated.incrementAndGet();
}
return (instance);
}
}
synchronized (instancePool) {
while (countAllocated.get() >= nInstances) {
// Allocate a new instance if possible, or else wait
if (nInstances < maxInstances) {
try {
instancePool.push(loadServlet());
nInstances++;
} catch (ServletException e) {}
} else {
try {
instancePool.wait();
} catch (InterruptedException e) {
// Ignore
}} }
countAllocated.incrementAndGet();
return instancePool.pop();
}}
/**
* 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.
*/
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; //注意此处,如果存在实例就直接返回
Servlet servlet;
try {
InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
try {
servlet = (Servlet) instanceManager.newInstance(servletClass);
} catch (ClassCastException e) {
}
if (servlet instanceof SingleThreadModel) {
if (instancePool == null) {
instancePool = new Stack<>();
} //此处,使用Stack存放STM的Servlet
singleThreadModel = true;
}
initServlet(servlet);
} finally {
}
return servlet;
}

那一个实现了SingleThreadModel接口的Servlet,一般会初始化多少个实例呢?
StandardWrapper类中有两个属性,其中maxInstance初始为20。所以上面的问题就有了答案。

/**
 * Does this servlet implement the SingleThreadModel interface?
 */
protected volatile boolean singleThreadModel = false;
/**
 * Maximum number of STM instances.
 */
protected int maxInstances = 20;

由于SingleThreadModel已经声明为废弃,官方不建议使用。我们这里只是让大家了解下。

总结下,一个Servlet究竟有几个实例呢?受如下几个原因影响:
是否在分布式环境中部署
是否实现SingleThreadModel,如果实现则最多会创建20个实例
在web.xml中声明了几次,即使同一个Servlet,如果声明多次,也会生成多个实例。

 

转自 

https://www.hollischuang.com/archives/849

https://www.jianshu.com/p/caba33d8f3fd

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐