Servlet 简介
Servlet 是 SUN 公司提供的一门用于开发动态 Web 资源的技术. SUN 公司在其 API 中提供了一个 Servlet 接口, 用户若想开发一个动态 Web 资源(即开发一个 Java 程序向浏览器输出数据), 需要完成以下 2 个步骤:
编写一个 Java 类, 实现 Servlet 接口;
把开发好的 Java 类部署到 Web 服务器中.
那么我们不仅要问, 写好的 Servlet 会在 Web 应用中的什么位置上呢? 位置如下如所示.
提示: 按照一种约定俗成的称呼习惯, 通常我们也把实现了 Servlet 接口的 Java 程序, 称之为 Servlet.
Servlet 快速入门 -- 使用 Servlet 向浏览器输出 "Hello World"
阅读 Servlet API 文档, 文档地址是. 文档里面有对 Servlet 接口的详细描述, 如下.
借助有道翻译为:
定义了所有 Servlet 必须实现的方法.
Servlet 是运行在一个 Web 服务器里的一个小型 Java 程序. Servlets 通常通过 HTTP(超文本传输协议)接收并响> 应来自 Web 客户端的请求.
要实现这个接口, 您可以编写一个继承了 javax.servlet.GenericServlet 的一般的 Servlet, 或者继承了 javax.servlet.http.HttpServlet 的 HTTP Servlet.
这个接口定义了方法来初始化一个 Servlet, 服务请求, 并从服务器删除 Servlet. 这些被称为生命周期方法> 并且按以下顺序依次调用:
Servlet 被构造, 然后用 init 方法初始化;
任何来自客户机的请求在 service 方法中处理;
Servlet 从服务中移除, 调用 destroy 方法销毁, 然后垃圾收集和完成.
除了生命周期方法, 该接口提供了 getServletConfig 方法 (Servlet 可以使用它来得到任何启动信息) 和 getServletInfo 方法(它允许 Servlet 返回自身的基本信息, 比如作者, 版本和版权).
这里面有一个专业术语 --life-cycle methods, 解释过来就是与生命周期相关的方法, 即生命周期中的某个特定时刻必定会执行的方法. 那么什么是对象的生命周期? 什么又是与生命周期相关的方法呢? 对象从创建到销毁经历的过程, 称之为对象的生命周期. 在对象生命周期过程中, 在特定时刻肯定会执行一些特定的方法, 这些方法称之为与生命周期相关的方法. 例如, 人从出生到死亡经历的过程, 为人的一个生命周期, 在人生命周期过程中, 必定有一些与生命周期息息相关的方法, 例如吃饭, 上学, 结婚等, 这些方法在人生命周期过程中某个特定时刻必定会执行, 所以这些方法是人生命周期相关的方法. 但不是说对象中的所有方法都与生命周期相关, 例如人自杀, 这个方法不是在生命周期中必定会执行的.
阅读完 Servlet API, 我们需要解决两个问题:
输出 Hello Servlet 的 Java 代码应该写在 Servlet 的哪个方法内?
如何向浏览器输出数据?
答案很明显:
输出 Hello Servlet 的 Java 代码应该写在 Servlet 的 service 方法中;
通过 ServletResponse 接口的实例中的 getOutputStream 方法获得输出流, 向 http 响应对象中写入数据, 服务器将 http 响应对象回送给浏览器, 浏览器解析数据并显示.
下面我们正式编写一个入门级的 Servlet. 首先在 Tomcat 服务器 webapps 目录下新建一个 Web 应用, 比如 myWeb(Web 应用所在目录), 在 myWeb 目录中新建一个 Web-INF 目录, 接着在 Web-INF 目录下新建一个 classes 目录, 在 classes 目录中新建一个 Java 应用程序 --FirstServlet.java, 代码如下:
- package cn.liayun;
- import java.io.*;
- import javax.servlet.*;
- public class FirstServlet extends GenericServlet {
- public void service(ServletRequest req, ServletResponse res) throws ServletException, java.io.IOException {
- OutputStream out = res.getOutputStream();
- out.write("Hello Servlet!!!".getBytes());
- }
- }
着编译 Java 应用程序, 如图:
所以, 我们需要将 Servlet 所用 Jar 包加载到 classpath 路径下, 如下图所示.
再在 Web-INF 目录中新建一个 Web.xml 文件, 配置 Servlet 的访问对外路径.
- <?xml version="1.0" encoding="UTF-8"?>
- <Web-App xmlns="http://xmlns.jcp.org/xml/ns/javaee"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
- http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1">
- <servlet>
- <servlet-name>FirstServlet</servlet-name>
- <servlet-class>cn.liayun.FirstServlet</servlet-class>
- </servlet>
- <servlet-mapping>
- <servlet-name>FirstServlet</servlet-name>
- <url-pattern>/FirstServlet</url-pattern>
- </servlet-mapping>
- </Web-App>
最后启动 Tomcat, 通过 Chrome 浏览器进行访问.
Servlet 的运行过程
Servlet 程序是由 Web 服务器调用的, Web 服务器收到客户端的 Servlet 访问请求后:
1Web 服务器首先检查是否已经装载并创建了该 Servlet 的实例对象. 如果是, 则直接执行第4步, 否则, 执行第2步;
2装载并创建该 Servlet 的一个实例对象;
3调用 Servlet 实例对象的 init()方法;
4创建一个用于封装 HTTP 请求消息的 HttpServletRequest 对象和一个代表 HTTP 响应消息的 HttpServletResponse 对象, 然后调用 Servlet 的 service()方法并将请求和响应对象作为参数传递进去;
5Web 应用程序被停止或重新启动之前, Servlet 引擎将卸载 Servlet, 并在卸载之前调用 Servlet 的 destroy()方法.
用动图来描述以上调用过程:
如果是用 UML 时序图来描述以上调用过程, 则如下:
注意: 上图并没画出 destory()方法. destory()方法会在 Web 容器移除 Servlet 时执行, 客户机第一次访问服务器时, 服务器会创建 Servlet 实例对象, 它就永远驻留在内存里面了, 等待客户机第二次访问, 这时有一个用户访问完 Servlet 之后, 此 Servlet 对象并不会被摧毁, destory()方法也就不会被执行.
一道面试题: 请说出 Servlet 的生命周期
Servlet 对象是用户第一次访问时创建, 对象创建之后就驻留在内存里面了, 响应后续的请求. Servlet 对象一旦被创建, init()方法就会被执行, 客户端的每次请求导致 service()方法被执行, Servlet 对象被摧毁时 (Web 服务器停止后或者 Web 应用从服务器里删除时),destory() 方法就会被执行.
在 Eclipse 中开发 Servlet
在 Eclipse 中新建一个 Dynamic Web Project 工程, Eclipse 会自动创建下图所示目录结构:
Servlet 接口实现类
对于 Servlet 接口, SUN 公司定义了两个默认实现类, 分别为 GenericServlet 和 HttpServlet.HttpServlet 指能够处理 HTTP 请求的 Servlet, 它在原有 Servlet 接口上添加了一些与 HTTP 协议相关的处理方法, 它比 Servlet 接口的功能更为强大. 因此开发人员在编写 Servlet 时, 通常应继承这个类, 而避免直接去实现 Servlet 接口. HttpServlet 在实现 Servlet 接口时, 覆写了 service 方法, 该方法体内的代码会自动判断用户的请求方式, 如为 GET 请求, 则调用 HttpServlet 的 doGet 方法, 如为 Post 请求, 则调用 doPost 方法. 因此, 开发人员在编写 Servlet 时, 通常只需要覆写 doGet 或 doPost 方法, 而不要去覆写 service 方法(温馨提示: 可阅读 HttpServlet API 文档).
借助有道翻译为:
提供了一个抽象类派生子类来创建一个适合于一个网站的 HTTP Servlet.HttpServlet 的子类必须覆盖至少一个方法, 通常是其中一个:
doGet, 如果 Servlet 支持 HTTP GET 请求
doPost,HTTP POST 请求
doPut,HTTP PUT 请求
doDelete,HTTP DELETE 请求
初始化和销毁, 管理 Sevlet 生命中被掌握的资源
getServletInfo,Servlet 用来提供关于其自身信息
几乎没有理由覆盖 service()方法. service()方法会处理标准 HTTP 请求, 通过派遣他们每个 HTTP 请求类型的处理程序方法(上述 doMethod 方法).
同样, 几乎没有理由覆盖 doOptions 和 doTrace 方法.
通过 Eclipse 创建和编写 Servlet
选中 cn.liayun 包, 右键→New→Servlet, 在 Eclipse 中创建和编写 Servlet 可参考下面一系列步骤:
这样, 我们就通过 Eclipse 帮我们创建好一个名字为 ServletSample 的 Servlet, 创建好的 ServletSample 里面会有如下代码:
- package cn.liayun;
- import java.io.IOException;
- import javax.servlet.ServletException;
- import javax.servlet.annotation.WebServlet;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- /**
- * Servlet implementation class ServletSample
- */
- @WebServlet("/ServletSample")
- public class ServletSample extends HttpServlet {
- private static final long serialVersionUID = 1L;
- /**
- * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
- */
- protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- // TODO Auto-generated method stub
- response.getWriter().append("Served at:").append(request.getContextPath());
- }
- /**
- * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
- */
- protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- // TODO Auto-generated method stub
- doGet(request, response);
- }
- }
这些代码都是 Eclipse 自动生成的, 而 Web.xml 文件中也多了 < servlet></servlet > 和 < servlet-mapping></servlet-mapping > 两对标签, 这两对标签是配置 ServletSample 的, 应如下所示:
- <servlet>
- <servlet-name>ServletSample</servlet-name>
- <servlet-class>cn.liayun.ServletSample</servlet-class>
- </servlet>
- <servlet-mapping>
- <servlet-name>ServletSample</servlet-name>
- <url-pattern>/ServletSample</url-pattern>
- </servlet-mapping>
注意: 照理说, Web.xml 文件中会多 < servlet></servlet > 和 < servlet-mapping></servlet-mapping > 这两对标签, 但是我的就没有, 而且使用的是注解 @WebServlet("/ServletSample"), 好像因为我使用的是 Servlet3.1 规范的缘故.
最后我们就可以通过浏览器访问 ServletSample 这个 Servlet 了, 访问的 URL 地址是 http://localhost:8080/day05/ServletSample.
Servlet 开发注意细节
如果你的 Eclipse 中有一个动态 Web 项目 TomcatTest, 当你使用 Eclipse 导入一个外部项目, 恰好这个项目名就是 TomcatTest, 这时你为了避免重名, 需要修改导入的项目名, 比如修改为 t_ TomcatTest, 然后你将其部署到 Tomcat 服务器中的 webapps 目录中, 该项目映射的虚拟目录名称仍然是 TomcatTest, 所以你需要修改其虚拟目录. 步骤如下:
Servlet 访问 URL 映射配置
由于客户端是通过 URL 地址访问 Web 服务器中的资源, 所以 Servlet 程序若想被外界访问, 必须把 Servlet 程序映射到一个 URL 地址上, 这个工作在 Web.xml 文件中使用 < servlet > 元素和 < servlet-mapping > 元素完成.<servlet > 元素用于注册 Servlet, 它包含有两个主要的子元素:<servlet-name > 和 < servlet-class>, 分别用于设置 Servlet 的注册名称和 Servlet 的完整类名. 一个 < servlet-mapping > 元素用于映射一个已注册的 Servlet 的一个对外访问路径, 它包含有两个子元素:<servlet-name > 和 < url-pattern>, 分别用于指定 Servlet 的注册名称和 Servlet 的对外访问路径. 例如:
- <servlet>
- <servlet-name>ServletDemo1</servlet-name>
- <servlet-class>cn.itcast.ServletDemo1</servlet-class>
- </servlet>
- <servlet-mapping>
- <servlet-name>ServletDemo1</servlet-name>
- <url-pattern>/ServletDemo1</url-pattern>
- </servlet-mapping>
同一个 Servlet 可以被映射到多个 URL 上, 即多个 < servlet-mapping > 元素的 < servlet-name > 子元素的设置值可以是同一个 Servlet 的注册名. 例如:
- <servlet>
- <servlet-name>ServletDemo1</servlet-name>
- <servlet-class>cn.itcast.ServletDemo1</servlet-class>
- </servlet>
- <servlet-mapping>
- <servlet-name>ServletDemo1</servlet-name>
- <url-pattern>/ServletDemo1</url-pattern>
- </servlet-mapping>
- <servlet-mapping>
- <servlet-name>ServletDemo1</servlet-name>
- <url-pattern>/aa</url-pattern>
- </servlet-mapping>
- <servlet-mapping>
- <servlet-name>ServletDemo1</servlet-name>
- <url-pattern>/1.HTML</url-pattern> <!-- 伪静态, 明显是一个动态 Web 资源, 但将其映射成静态 Web 资源的名称 -->
- </servlet-mapping>
温馨提示: 一个 Web 应用的 Web.xml 文件内容一经修改, 不需要重新发布, 服务器会自动监测 Web.xml 的改动, 只要 Web.xml 文件的内容修改, 服务器就会自动加载. 原因是在 Tomcat 服务器的 conf/context.xml 文件中, 有如下关键代码:
根据 Tomcat 服务器文档可知, 在 conf/context.xml 文件中, Context 元素信息被所有的 Web 应用加载. 即 Context 元素的配置信息会被所有 Web 应用程序所共享. 所以所有的 Web 应用会监测 Web.xml 的改动, 只要 Web.xml 文件的内容一旦修改, 服务器就会自动重新加载.
通过上面的配置, 当我们想访问名称是 ServletDemo1 的 Servlet 时, 可以使用如下的几个地址去访问:
- http://localhost:8080/day05/ServletDemo1;
- http://localhost:8080/day05/aa;
- http://localhost:8080/day05/1.HTML.
ServletDemo1 被映射到了多个 URL 上.
Servlet 访问 URL 使用 * 通配符映射
在 Servlet 映射到的 URL 中也可以使用 * 通配符, 但是只能有两种固定的格式: 一种格式是 "*. 扩展名", 另一种格式是以正斜杠 (/) 开头并以 "*" 结尾. 例如:
- <servlet-mapping>
- <servlet-name>AnyName</servlet-name>
- <url-pattern>*.do</url-pattern>
- </servlet-mapping>
- <servlet-mapping>
- <servlet-name>AnyName</servlet-name>
- <url-pattern>/action/*</url-pattern>
- </servlet-mapping>
对于如下的一些映射关系:
Servlet1 映射到 / abc/*;
Servlet2 映射到 /*;
Servlet3 映射到 / abc;
Servlet4 映射到 *.do.
有如下问题:
当请求 URL 为 "/abc/a.html","/abc/*" 和 "/*" 都匹配, 哪个 Servlet 响应?--Servlet 引擎将调用 Servlet1;
当请求 URL 为 "/abc" 时,"/abc/*","/*" 和 "/abc" 都匹配, 哪个 Servlet 响应?--Servlet 引擎将调用 Servlet3;
当请求 URL 为 "/abc/a.do" 时,"/abc/*","/*" 和 "*.do" 都匹配, 哪个 Servlet 响应?--Servlet 引擎将调用 Servlet1;
当请求 URL 为 "/a.do" 时,"/*" 和 "*.do" 都匹配, 哪个 Servlet 响应?--Servlet 引擎将调用 Servlet2;
当请求 URL 为 "/xxx/yyy/a.do" 时,"/*" 和 "*.do" 都匹配, 哪个 Servlet 响应?--Servlet 引擎将调用 Servlet2.
** 结论: 匹配的原则就是 "谁长得更像就找谁","*.do"-- 这种 * 在前面的时候优先级最低.**
Servlet 与普通 Java 类的区别
Servlet 是一个供其他 Java 程序 (Servlet 引擎) 调用的 Java 类, 它不能独立运行, 它的运行完全由 Servlet 引擎来控制和调度. 针对客户端的多次 Servlet 请求, 通常情况下, 服务器只会创建一个 Servlet 实例对象, 也就是说 Servlet 实例对象一旦创建, 它就会驻留在内存中, 为后续的其它请求服务, 直至 Web 容器退出, Servlet 实例对象才会销毁. 验证如下:
新建一个 Servlet--ServletDemo3, 并覆盖 init()和 destroy()方法;
- package cn.liayun;
- import java.io.IOException;
- import javax.servlet.ServletConfig;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- public class ServletDemo3 extends HttpServlet {
- private static final long serialVersionUID = 1L;
- @Override
- public void init(ServletConfig config) throws ServletException {
- super.init(config);
- System.out.println("init");
- }
- protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- response.getOutputStream().write("haha".getBytes());
- }
- protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- doGet(request, response);
- }
- @Override
- public void destroy() {
- System.out.println("destroy");
- }
- }
将项目部署到服务器中, 启动服务器, 发现没有输出 init, 说明启动服务器时, Servlet 实例对象并没有被创建. 此时, 通过浏览器进行访问, 会发现控制台输出 init, 如下:
此时再打开一个浏览器进行访问, 仍然只会输出一个 init, 说明针对客户端的多次 Servlet 请求, 通常情况下, 服务器只会创建一个 Servlet 实例对象.
当 Web 服务器停止后或者 Web 应用从服务器里删除时, destroy()方法就会被执行;
在 Web 服务器停止前, Servlet 实例对象就会被摧毁.
在 Servlet 的整个生命周期内, Servlet 的 init 方法只被调用一次. 而对一个 Servlet 的每次访问请求都导致 Servlet 引擎调用一次 Servlet 的 service 方法. 对于每次访问请求, Servlet 引擎都会创建一个新的 HttpServletRequest 请求对象和一个新的 HttpServletResponse 响应对象, 然后将这两个对象作为参数传递给它调用的 Servlet 的 service()方法, service 方法再根据请求方式分别调用 doXXX 方法.
如果在元素中配置了一个元素, 那 Web 应用程序在启动时, 就会装载并创建 Servlet 的实例对象, 以及调用 Servlet 实例对象的 init()方法. 例如:
- <servlet>
- <servlet-name>ServletDemo3</servlet-name>
- <servlet-class>cn.itcast.ServletDemo3</servlet-class>
- <load-on-startup>1</load-on-startup>
- </servlet>
此时在启动服务器的过程中, 会在控制台看到:
温馨提示:<load-on-startup > 元素配置的数必须为正整数, 数字越小, Servlet 越优先创建. 它的用途: 可为 Web 应用写一个 InitServlet, 这个 Servlet 配置为启动时装载, 为整个 Web 应用创建必要的数据库表和数据.
缺省 Servlet
如果某个 Servlet 的映射路径仅仅为一个正斜杠(/), 那么这个 Servlet 就成为当前 Web 应用程序的缺省 Servlet. 凡是在 Web.xml 文件中找不到匹配的 < servlet-mapping > 元素的 URL, 它们的访问请求都将交给缺省 Servlet 处理, 也就是说, 缺省 Servlet 用于处理所有其他 Servlet 都不处理的访问请求. 例如:
- <servlet>
- <servlet-name>ServletDemo3</servlet-name>
- <servlet-class>cn.itcast.ServletDemo3</servlet-class>
- <load-on-startup>1</load-on-startup>
- </servlet>
- <!-- 将 ServletDemo3 配置成缺省 Servlet -->
- <servlet-mapping>
- <servlet-name>ServletDemo3</servlet-name>
- <url-pattern>/</url-pattern>
- </servlet-mapping>
当访问不存在的 Servlet 时, 就使用配置的默认 Servlet 进行处理, 如下图所示:
在 < Tomcat 的安装目录>\conf\Web.xml 文件中, 注册了一个名称为 org.apache.catalina.servlets.DefaultServlet 的 Servlet, 并将这个 Servlet 设置为了缺省 Servlet.
- <servlet>
- <servlet-name>default</servlet-name>
- <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
- <init-param>
- <param-name>debug</param-name>
- <param-value>0</param-value>
- </init-param>
- <init-param>
- <param-name>listings</param-name>
- <param-value>false</param-value>
- </init-param>
- <load-on-startup>1</load-on-startup>
- </servlet>
- <!-- The mapping for the default servlet -->
- <servlet-mapping>
- <servlet-name>default</servlet-name>
- <url-pattern>/</url-pattern>
- </servlet-mapping>
当访问 Tomcat 服务器中的某个静态 HTML 文件和图片时, 实际上是在访问这个缺省 Servlet(服务器中的 HTML 文件数据的读取由缺省 Servlet 完成).
Servlet 的线程安全问题
当多个客户端并发访问同一个 Servlet 时, Web 服务器会为每一个客户端的访问请求创建一个线程, 并在这个线程上调用 Servlet 的 service 方法, 因此 service 方法内如果访问了同一个资源的话, 就有可能引发线程安全问题. 下面我会举例来说明.
当 Servlet 不存在线程安全问题时
下面是不存在线程安全问题的代码.
- package cn.liayun;
- import java.io.IOException;
- import javax.servlet.ServletException;
- import javax.servlet.annotation.WebServlet;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- @WebServlet("/ServletSample")
- public class ServletSample extends HttpServlet {
- protected void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- int i = 0;
- i++;
- response.getOutputStream().write((i + "").getBytes());
- }
- protected void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- doGet(request, response);
- }
- }
当多线程并发访问这个方法里面的代码时, 会存在线程安全问题吗? 显然不会, i 变量被多个线程并发访问, 但是没有线程安全问题, 因为 i 是 doGet 方法里面的局部变量, 当有多个线程并发访问 doGet 方法时, 每一个线程里面都有自己的 i 变量, 各个线程操作的都是自己的 i 变量, 所以不存在线程安全问题. 多线程并发访问某一个方法的时候, 如果在方法内部定义了一些资源(变量, 集合等), 那么每一个线程都有这些东西, 所以就不存在线程安全问题.
当 Servlet 存在线程安全问题时
下面是存在线程安全问题的代码.
- package cn.liayun;
- import java.io.IOException;
- import javax.servlet.ServletException;
- import javax.servlet.annotation.WebServlet;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- @WebServlet("/ServletSample")
- public class ServletSample extends HttpServlet {
- private int i = 0;
- protected void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- i++;
- try {
- Thread.sleep(1000 * 10);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- response.getOutputStream().write((i + "").getBytes());
- }
- protected void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- doGet(request, response);
- }
- }
把 i 定义成全局变量, 当多个线程并发访问变量 i 时, 就会存在线程安全问题了. 线程安全问题只存在多个线程并发操作同一个资源的情况下, 所以在编写 Servlet 的时候, 如果并发访问某一个资源(变量, 集合等), 就会存在线程安全问题, 那么该如何解决这个问题呢? 可使用同步代码块.
- package cn.liayun;
- import java.io.IOException;
- import javax.servlet.ServletException;
- import javax.servlet.annotation.WebServlet;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- @WebServlet("/ServletSample")
- public class ServletSample extends HttpServlet {
- private int i = 0;// 共享资源
- protected void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- i++;
- synchronized (this) {
- try {
- Thread.sleep(1000 * 10);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- response.getOutputStream().write((i + "").getBytes());
- }
- protected void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- doGet(request, response);
- }
- }
加了 synchronized 后, 并发访问 i 时就不存在线程安全问题了, 为什么加了 synchronized 后就没有线程安全问题了呢? 原因: 假如现在有一个线程访问 Servlet 对象, 那么它就先拿到了 Servlet 对象的那把锁, 等到它执行完之后才会把锁还给 Servlet 对象, 由于是它先拿到了 Servlet 对象的那把锁, 所以当有别的线程来访问这个 Servlet 对象时, 由于锁已经被之前的线程拿走了, 后面的线程只能排队等候了.
以上这种做法是给 Servlet 对象加了一把锁, 保证任何时候都只有一个线程在访问该 Servlet 对象里面的资源, 这样就不存在线程安全问题了. 这种做法虽然解决了线程安全问题, 但是编写 Servlet 却万万不能用这种方式处理线程安全问题, 假如有 9999 个人同时访问这个 Servlet, 那么这 9999 个人必须按先后顺序排队轮流访问.
针对 Servlet 的线程安全问题, SUN 公司是提供有解决方案的: 让 Servlet 去实现一个 SingleThreadModel 接口, 如果某个 Servlet 实现了 SingleThreadModel 接口, 那么 Servlet 引擎将以单线程模式来调用其 service 方法. 查看 Sevlet 的 API 可以看到, SingleThreadModel 接口中没有定义任何方法和常量, 在 Java 中, 把没有定义任何方法和常量的接口称之为标记接口, 经常看到的一个最典型的标记接口就是 "Serializable", 这个接口也是没有定义任何方法和常量的, 标记接口在 Java 中有什么用呢? 主要作用就是给某个对象打上一个标志, 告诉 JVM, 这个对象可以做什么, 比如实现了 "Serializable" 接口的类的对象就可以被序列化, 还有一个 "Cloneable" 接口, 这个也是一个标记接口, 在默认情况下, Java 中的对象是不允许被克隆的, 就像现实生活中的人一样, 不允许克隆, 但是只要实现了 "Cloneable" 接口, 那么对象就可以被克隆了. SingleThreadModel 接口中没有定义任何方法, 只要在 Servlet 类的定义中增加实现 SingleThreadModel 接口的声明即可.
对于实现了 SingleThreadModel 接口的 Servlet,Servlet 引擎仍然支持对该 Servlet 的多线程并发访问, 其采用的方式是产生多个 Servlet 实例对象, 并发的每个线程分别调用一个独立的 Servlet 实例对象. 实现 SingleThreadModel 接口并不能真正解决 Servlet 的线程安全问题, 因为 Servlet 引擎会创建多个 Servlet 实例对象, 而真正意义上解决多线程安全问题是指一个 Servlet 实例对象被多个线程同时调用的问题. 事实上, 在 Servlet API 2.4 中, 已经将 SingleThreadModel 标记为 Deprecated(过时的).
以上代码还要注意异常的处理, 代码 Thread.sleep(1000*4); 只能 try 不能抛, 因为子类在覆盖父类的方法时, 不能抛出比父类更多的异常; 并且 catch 之后, 后台记录异常的同时并给用户一个友好提示, 因为用户访问的是一个网页.
来源: https://www.cnblogs.com/rolandlee/p/10756573.html