DRP(四)——线程安全的Servlet
Posted 崔伟林
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DRP(四)——线程安全的Servlet相关的知识,希望对你有一定的参考价值。
多线程的Seervlet模型
Servlet规范定义,在默认情况下(Servlet不是在分布式的环境中部署),Servlet容器对声明的每一个Servlet,只创建一个实例。如果有多个客户请求同时访问这个Servlet,Servlet容器如何处理这多个请求呢?答案是采用多线程,Servlet容器维护一个线程池来服务请求。线程池实际上是等待执行代码的一组线程,这些线程叫做工作者线程。Servlet容器使用一个调度者线程来管理工作者线程。当容器接收到一个访问Servlet的请求,调度者线程从线程池中选取一个工作者线程,将请求传递给该线程,然后由这个线程执行Servlet的service方法,如下图:
当容器接收到另一个请求时,调度者线程将从池中选取另一个线程来服务新的请求。
由于Servlet容器采用单实例多线程的方式(这是Servlet容器默认的行为),最大限度地减少了产生Servlet实例的开销,显著地提升了对请求的响应时间。对于Tomcat,可以在server.xml文件中<Connector>元素中设置线程池中线程的数目。
变量的线程安全
<span style="font-size:24px;">package org.sunxin.ch02.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class WelcomeServlet extends HttpServlet{
private String greeting;
String username="";
public void init(){
greeting = getInitParameter("greeting");
}
public void doGet(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{
req.setCharacterEncoding("gb2312");
username= req.getParameter("username");
String welcomeInfo=greeting + "," + username;
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html><head><title>");
out.println("Welcome page");
out.println("</title><head>");
out.println("<body>");
out.println(welcomeInfo);
out.println("</body></html>");
out.close();
}
public void doPost(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{
doGet(req,resp);
}
}</span><span style="font-size: 18px;">
</span>
这段代码主要是向用户显示欢迎信息,然而这段代码有一个潜在的线程安全问题。当用户A和B同时访问这个Servlet时,会出现:
(1)Servlet容器分配一个工作者线程T1来服务用户A的请求,分配另一个工作者线程T2服务用户B的请求。
(2)操作系统首先调度T1运行。
(3)T1执行代码后,从请求对象中获取用户的姓名,保存到变量user中,现在user的值是A。
(4)当T1试图执行下面的代码时,时间片到期,操作系统调度T2运行。
(5)T2执行代码后,从请求对象中获取用户的姓名,保存到变量user中,现在user的值是A.
(6)T2继续执行后面的代码,向用户B输出“Welcome you ,B”。
(7)T2执行完毕,操作系统重新调度T1执行,T1从上次执行的代码中断处继续往下执行,因为这个时候user变量的值已经变成了B,所以T1向用户A发送“Welcome you,B”。
解决这个问题,可以采取两种方式:第一种是将username定义为本地变量,
<span style="font-size:24px;">package org.sunxin.ch02.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class WelcomeServlet extends HttpServlet{
private String greeting;
public void init(){
greeting = getInitParameter("greeting");
}
public void doGet(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{
req.setCharacterEncoding("gb2312");
String username= req.getParameter("username");
String welcomeInfo=greeting + "," + username;
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html><head><title>");
out.println("Welcome page");
out.println("</title><head>");
out.println("<body>");
out.println(welcomeInfo);
out.println("</body></html>");
out.close();
}
public void doPost(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{
doGet(req,resp);
}
}</span>
第二种方式是同步doGet()方法
<span style="font-size:24px;">package org.sunxin.ch02.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class WelcomeServlet extends HttpServlet{
private String greeting;
String username="";
public void init(){
greeting = getInitParameter("greeting");
}
public synchronized void doGet(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{
req.setCharacterEncoding("gb2312");
username= req.getParameter("username");
String welcomeInfo=greeting + "," + username;
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html><head><title>");
out.println("Welcome page");
out.println("</title><head>");
out.println("<body>");
out.println(welcomeInfo);
out.println("</body></html>");
out.close();
}
public void doPost(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{
doGet(req,resp);
}
}</span>
因为使用了同步,就可以防止多个线程同时调用doGet()方法,也就避免了在请求处理过程中,user实例变量被其他线程修改的可能。不过对doGet()方法使用同步,意味着访问同一个Servlet的请求将排队,一个线程处理完请求后,才能执行另一个线程,这将严重影响性能,所以我们几乎不采用这种方式。
举例:
在Tomcat文档中描述过的“Connection ClosedException”,代码如下
package org.sunxin.ch02.servlet;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
public class testServlet extends HttpServlet {
DataSource ds = null;
public void init(){
try{
Context ctx = new InitialContext();
ds=(DataSource)ctx.lookup("java:comp/env/jdbc/bookstore");
}catch(Exception e){
e.printStackTrace();
}
}
public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException,java.io.IOException{
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try{
conn=ds.getConnection();//从连接池中得到连接
stmt = conn.createStatement();
rs = stmt.executeQuery("....");
//.....省略
rs.close();
stmt.close();
conn.close();
}catch(Exception e){
System.out.println(e);
}finally{
if(rs != null){
try{
rs.close();
}catch(Exception e){
System.out.println(e);
}
}
if(stmt != null){
try{
stmt.close();
}catch(Exception e){
System.out.println(e);
}
}
if(conn != null){
try{
conn.close();
}catch(Exception e){
System.out.println(e);
}
}
}
}
}
这段代码导致异常产生过程如下:
(1)当服务一个请求的线程T1运行时,从连接池中得到一个数据库连接
(2)在线程T1中,当执行完数据库访问操作后,关闭数据库
(3)此时,操作系统调度另一个线程T2运行
(4)T2为另一个访问该Servlet的请求服务,从连接池中得到一个数据库连接,而这个连接郑浩是刚才在T1线程中调用close()方法后,放回池中的连接
(5)此时,操作系统调度线程T1运行
(6)T1继续执行后面的代码,在finally语句中,再次关闭数据库连接。要注意,调用Connection对象后的close()方法只是关闭数据库连接,而对象本身并不为空,所以finally语句中的关闭操作才又一次执行
(7)此时,操作系统调度线程T2运行。
(8)线程T2视图使用数据库连接,但却失败了,因为T1关闭了该连接
(有篇文章推荐:java.sql.Connection的close方法究竟干了啥)
要避免上述的情况,就要求我们正确的编写代码,在关闭数据库对象后,将该对象设为null。正确代码如下:
<span style="font-size:24px;">package org.sunxin.ch02.servlet;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
public class testServlet extends HttpServlet {
DataSource ds = null;
public void init(){
try{
Context ctx = new InitialContext();
ds=(DataSource)ctx.lookup("java:comp/env/jdbc/bookstore");
}catch(Exception e){
e.printStackTrace();
}
}
public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException,java.io.IOException{
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try{
conn=ds.getConnection();//从连接池中得到连接
stmt = conn.createStatement();
rs = stmt.executeQuery("....");
//.....省略
rs.close();
rs=null;
stmt.close();
stmt=null;
conn.close();//连接被放回连接池
conn=null; //确保我们不会关闭连接两次
}catch(Exception e){
System.out.println(e);
}finally{
if(rs != null){
try{
rs.close();
}catch(Exception e){
System.out.println(e);
}
}
if(stmt != null){
try{
stmt.close();
}catch(Exception e){
System.out.println(e);
}
}
if(conn != null){
try{
conn.close();
}catch(Exception e){
System.out.println(e);
}
}
}
}
}</span>
属性的线程安全
在Servlet中,可以访问保存在SeervletContext,HttpSession和ServletRequest对象中的属性,这三种对象都提供了getAttribute()和setAttribute()方法用于读取和设置属性。
ServletContext
ServletContext对象可以被Web应用程序中所有的Servlet访问,多个线程可以同时在Servlet上下文中设置或读取属性,这将导致存储数据的不一致。例如:有两个Servlet,LoginServlet和DisplayUsersServlet。LoginServlet负责验证用户,并将用户名添加到保存在Servlet上下文中的列表中,当用户退出的时候,LoginServlet从列表中删除用户名。代码如下:
LoginServlet:
<span style="font-size:24px;">public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException,java.io.IOException{
String username = //验证用户
if(authenticated){
List list= (List)getServletContext().getAttribute("usersList");
}else if(logout){
//从用户列表中删除用户名
List list = (List)getServletContext().getAttribute("usersList");
list.remove(username);
}
} </span>
DisplayUsersServlet:
<span style="font-size:24px;">public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException,java.io.IOException{
PrintWriter out = res.getWriter;
List list = (List)getServletContext().getAttribute("usersList");
int count = list.size();
out.println("<html><body>");
for(int i=0; i<count;i++){
out.println(list.get(i)+"<br>");
}
out.println("</body></html>");
out.close();
} </span>
usersList属性在任何时候都可以被所有的Servlet访问,因此,当DisplayUsersServlet在循环输出用户名的时候,LoginServlet可能从用户列表中删除了一个用户名,这将导致抛出IndexOutOfBoundsExcetption异常。ServletContext属性的访问不是线程安全的,为了避免出现问题,可以对用户列表的访问进行同步或者对用户列表产生一个拷贝。
HttpSession
用户可以打开多个同属于一个进程的浏览器窗口,在这些窗口中的访问请求,属于同一个Session,为了同时处理多个这样的请求,Servlet容器会创建多个线程,而在这些线程中,就可以同时访问到Sesion对象的属性。
举一个购物车例子,如果用户一个浏览器中删除一个条目,同时又在另一个浏览器窗口中查看购物车中的所有条目,这将导致抛出IndexOutOfBoundsExcetption,要避免这个问题,而已对Session的访问进行同步。
ServletRequest
因为Servlet容器对它所接收到的每一个请求,都创建一个新的ServletRequest对象,所以ServletRequest对象只在一个线程中被访问。因为只有一个线程服务请求,所以请求对象的属性访问是线程安全的。
总结
真是大开眼界,从中学到很多,分享给大家。
以上是关于DRP(四)——线程安全的Servlet的主要内容,如果未能解决你的问题,请参考以下文章