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的主要内容,如果未能解决你的问题,请参考以下文章

关于Servlet线程安全的问题

多线程(四):线程安全

Servlet的线程安全

Servlet和Struts2的线程安全问题

如何用SingleThreadModel解决多线程安全问题

Servlet的线程安全问题