关于Servlet线程安全的详解

来源:互联网 发布:淘宝商城女装特价冬装 编辑:程序博客网 时间:2024/06/01 15:22
Servlet规范定义,在默认情况下(Servlet不是在分布式的环境中部署),Servlet容器对声明的每一个Servlet,只创建一个实例。如果有多个客户请求同时访问这个Servlet,Servlet容器如何处理这多个请求呢?答案是采用多线程,Servlet容器维护了一个多线程池来服务请求。线程池实际上是等待执行代码的一组线程,这些线程叫做工作者线程(Worker Thread)。Servlet容器使用一个调度者线程(Dispatcher Thread)来管理工作者线程。当容器接收到一个访问Servlet的请求,调度者线程从线程池中选取一个工作者线程,将请求传递给该线程,然后由这个线程执行Servlet的service()方法。
    当这个线程正在执行的时候,容器收到了另一个请求,调度者线程将从池中选取另一个线程来服务新的请求。要注意的是,Servlet容器并不关心这第二个请求是访问同一个Servlet还是另一个Servlet。因此,如果容器同时收到访问是同一个Servlet的多个请求,那么这个Servlet的service()方法将在多个线程中并发的执行。
    由于Servlet容器采用了单实例多线程的方式(这是Servlet容器默认的行为),最大限度地减少了产生Servlet实例的开销。虽然提高了性能,但同时也对Servlet的开发者提出了更高的要求,在开发Servlet时,要注意线程安全的问题。首先我们先一例子:
  1. package threadsafe;

  2. import java.io.IOException;
  3. import java.io.PrintWriter;

  4. import javax.servlet.ServletException;
  5. import javax.servlet.http.HttpServlet;
  6. import javax.servlet.http.HttpServletRequest;
  7. import javax.servlet.http.HttpServletResponse;

  8. /**
  9.  Servlet implementation class WelcomeServlet
  10.  */
  11. public class WelcomeServlet extends HttpServlet {
  12.     String user "";
  13.     protected void doGet(HttpServletRequest request,
  14.             HttpServletResponse response) throws ServletException, IOException {
  15.         
  16.         user request.getParameter("user");
  17.         String welcomeInfo "Welcome you, user;

  18.         response.setContentType("text/html");
  19.         PrintWriter out response.getWriter();

  20.         out.println("<html><head><title>Welcome Page</title></head><body>");
  21.         out.println(welcomeInfo);
  22.         out.println("</body></html>");
  23.         out.close();
  24.     }

  25. }
    这段代码主要向用户显示欢迎信息,但这段代码存在一个潜在的线程安全问题,假设两个用户A和B同时访问这个Servlet,
(1)Servlet容器分配一个工作者线程T1来服务用户A的请求,分配另一个工作者线程T2来服务用户B的请求。
(2)操作系统首先调用T1运行。
(3)T1执行代码到第19行,从请求对象中获取用户姓名,保存到变量user中,现在user的值是A。
(4)当T1试图执行下面的代码时,时间片到期,操作系统调度T2运行。
(5)T2执行代码到第19行,从请求对象中获取用户姓名,保存到变量user中,现在user的值是B,而T1执行时保存的值丢失了。
(6)T2继续执行后面的代码,向用户B输出“Welcome you, B”。
(7)T2执行完毕,操作系统重新调度T1执行,T1从上次执行的代码中断处继续往下执行,因为这个时候user变量的值已经变成了B,所以T1向用户A发送“Welcome you, B”。
    这个问题的产生是因为user是一个实例变量,它可以在多个同时运行doGet()方法的线程中共享,在请求处理期间,该变量的值随时会被某个线程所改变。
    要解决这个问题,可以采取两种方式:第一种方式是将user定义为本地变量:
  1. package threadsafe;

  2. import java.io.IOException;
  3. import java.io.PrintWriter;

  4. import javax.servlet.ServletException;
  5. import javax.servlet.http.HttpServlet;
  6. import javax.servlet.http.HttpServletRequest;
  7. import javax.servlet.http.HttpServletResponse;

  8. /**
  9.  * Servlet implementation class WelcomeServlet
  10.  */
  11. public class WelcomeServlet extends HttpServlet {
  12.     

  13.     protected void doGet(HttpServletRequest request,
  14.             HttpServletResponse response) throws ServletException, IOException {
  15.         String user "";
  16.         user request.getParameter("user");
  17.         String welcomeInfo "Welcome you, user;

  18.         response.setContentType("text/html");
  19.         PrintWriter out response.getWriter();

  20.         out.println("<html><head><title>Welcome Page</title></head><body>");
  21.         out.println(welcomeInfo);
  22.         out.println("</body></html>");
  23.         out.close();
  24.     }

  25. }
     因为user是本地变量,每一个线程都将拥有user变量的拷贝,线程对自己栈中的本地变量的改变不会影响其他线程的本地变量的拷贝,因此,在请求处理过程中,user的值不会被别的线程所改变。在Servlet的开发中,本地变量总是线程安全的。
    第二种方式是同步doGet()方法:
  1. package threadsafe;

  2. import java.io.IOException;
  3. import java.io.PrintWriter;

  4. import javax.servlet.ServletException;
  5. import javax.servlet.http.HttpServlet;
  6. import javax.servlet.http.HttpServletRequest;
  7. import javax.servlet.http.HttpServletResponse;

  8. /**
  9.  * Servlet implementation class WelcomeServlet
  10.  */
  11. public class WelcomeServlet extends HttpServlet {
  12.     String user "";

  13.     protected synchronized void doGet(HttpServletRequest request,
  14.             HttpServletResponse response) throws ServletException, IOException {
  15.         
  16.         user request.getParameter("user");
  17.         String welcomeInfo "Welcome you, user;

  18.         response.setContentType("text/html");
  19.         PrintWriter out response.getWriter();

  20.         out.println("<html><head><title>Welcome Page</title></head><body>");
  21.         out.println(welcomeInfo);
  22.         out.println("</body></html>");
  23.         out.close();
  24.     }

  25. }
     因为使用了同步,就可以防止多个线程同时调用doGet()方法,也就避免了在请求处理过程中,user实例变量被其他线程修改的可能。不过,对doGet()方法使用同步,意味着访问同一个Servlet的请求将排队,一个线程处理完请求后,才能执行另一个线程,这将严重影响性能,所以几乎不采用这种方式。
    对于Servlet中的类变量(静态变量),因为它们在所有属于该类的实例中共享,所以也不是线程安全的。类变量在Servlet中常被用于存储只读的或常量的数据,例如存储JDBC驱动程序类名,连接URL等。
    再如,我们在使用数据源连接数据库的时候也会出现线程安全的问题:
  1. package threadsafe;

  2. import java.beans.Statement;
  3. import java.io.IOException;
  4. import java.sql.Connection;
  5. import java.sql.ResultSet;

  6. import javax.naming.Context;
  7. import javax.naming.InitialContext;
  8. import javax.servlet.ServletException;
  9. import javax.servlet.http.HttpServlet;
  10. import javax.servlet.http.HttpServletRequest;
  11. import javax.servlet.http.HttpServletResponse;
  12. import javax.sql.DataSource;

  13. /**
  14.  * Servlet implementation class TestServlet
  15.  */
  16. public class TestServlet extends HttpServlet {

  17.     DataSource ds null;

  18.     public void init() {
  19.         try {
  20.             Context ctx new InitialContext();
  21.             ds (DataSource) ctx.lookup("java:comp/env/jdbc/bookstore");
  22.         catch (Exception e) {
  23.             e.printStackTrace();
  24.         }
  25.     }

  26.     @Override
  27.     protected void service(HttpServletRequest request,
  28.             HttpServletResponse response) throws ServletException, IOException {
  29.         Connection conn null;
  30.         java.sql.Statement stmt null;
  31.         ResultSet rs null;
  32.         try {
  33.             conn ds.getConnection();  // 从连接池得到连接
  34.             stmt conn.createStatement();
  35.             rs stmt.executeQuery("...");
  36.             // do something...
  37.             
  38.             rs.close();
  39.             stmt.close();
  40.             conn.close(); // 连接被放回连接池
  41.         catch (Exception e) {
  42.             System.out.println(e);
  43.         }

  44.         finally {
  45.             if (rs != null) {
  46.                 try {
  47.                     rs.close();
  48.                 catch (Exception e) {
  49.                     System.out.println(e);
  50.                 }
  51.             }
  52.             if (stmt != null) {
  53.                 try {
  54.                     stmt.close();
  55.                 catch (Exception e) {
  56.                     System.out.println(e);
  57.                 }
  58.             }
  59.             if (conn != null) {
  60.                 try {
  61.                     conn.close();
  62.                 catch (Exception e) {
  63.                     System.out.println(e);
  64.                 }
  65.             }
  66.         }
  67.     }
  68. }

    从代码本身看,好像没什么问题,因为我们没有定义任何的实例变量,然而这段代码在执行时可能会发生连接已经关闭的异常,导致异常产生的过程如下:
(1)当服务一个请求的线程T1运行时,从连接池中得到一个数据库连接(第39行)。
(2)在线程T1中,当执行完数据库访问操作后,关闭数据库(第46行)。
(3)此时,操作系统调度另一个线程T2执行。
(4)T2为另一个访问该Servlet的请求服务,从连接池中得到一个数据库连接(第39行),而这个连接正好是刚才在T1线程中调用close()方法后,放回池中的连接。
(5)此时,操作系统调度线程T1运行。
(6)T1继续执行后面的代码,在finally语句中,再次关闭数据库连接(第68行)。要注意,调用Connection对象的close()方法只是关闭数据库连接,而对象本身并不为空,所以finally语句中的关闭操作才又一次执行。
(7)此时,操作系统调度线程T2运行。
(8)线程T2试图使用数据库连接,但却失败了,因为T1关闭了该连接。
    上述问题是在使用连接池的情况下才会发生,要避免出现上述的情况,就要求我们正确地编写代码,在关闭数据库对象后,将该对象设为null。如下(第45行,47行,49行所示):
  1. package threadsafe;

  2. import java.beans.Statement;
  3. import java.io.IOException;
  4. import java.sql.Connection;
  5. import java.sql.ResultSet;

  6. import javax.naming.Context;
  7. import javax.naming.InitialContext;
  8. import javax.servlet.ServletException;
  9. import javax.servlet.http.HttpServlet;
  10. import javax.servlet.http.HttpServletRequest;
  11. import javax.servlet.http.HttpServletResponse;
  12. import javax.sql.DataSource;

  13. /**
  14.  * Servlet implementation class TestServlet
  15.  */
  16. public class TestServlet extends HttpServlet {

  17.     DataSource ds null;

  18.     public void init() {
  19.         try {
  20.             Context ctx new InitialContext();
  21.             ds (DataSource) ctx.lookup("java:comp/env/jdbc/bookstore");
  22.         catch (Exception e) {
  23.             e.printStackTrace();
  24.         }
  25.     }

  26.     @Override
  27.     protected void service(HttpServletRequest request,
  28.             HttpServletResponse response) throws ServletException, IOException {
  29.         Connection conn null;
  30.         java.sql.Statement stmt null;
  31.         ResultSet rs null;
  32.         try {
  33.             conn ds.getConnection();  // 从连接池得到连接
  34.             stmt conn.createStatement();
  35.             rs stmt.executeQuery("...");
  36.             // do something...
  37.             
  38.             rs.close();
  39.             rs null;
  40.             stmt.close();
  41.             stmt null;
  42.             conn.close(); // 连接被放回连接池
  43.             conn null;
  44.         catch (Exception e) {
  45.             System.out.println(e);
  46.         }

  47.         finally {
  48.             if (rs != null) {
  49.                 try {
  50.                     rs.close();
  51.                 catch (Exception e) {
  52.                     System.out.println(e);
  53.                 }
  54.             }
  55.             if (stmt != null) {
  56.                 try {
  57.                     stmt.close();
  58.                 catch (Exception e) {
  59.                     System.out.println(e);
  60.                 }
  61.             }
  62.             if (conn != null) {
  63.                 try {
  64.                     conn.close();
  65.                 catch (Exception e) {
  66.                     System.out.println(e);
  67.                 }
  68.             }
  69.         }
  70.     }
  71. }
    为了开发线程安全的Servlet,我们应该尽可能地做到:
(1)尽可能地在Servlet中只使用本地变量。
(2)应该只使用只读的实例变量和静态变量。
(3)不要在Servlet中创建自己的线程。
(4)修改共享对象时,一定要使用同步,尽可能地缩小同步代码的范围,不要直接在service()方法或doXXX()方法上进行同步,以免影响性能。
(5)如果在多个不同的Servlet中,要对外部对象(例如,文件)进行修改操作,一定要加锁,做到互斥的访问。
0 0
原创粉丝点击