精华内容
下载资源
问答
  • 摘 要:介绍了Servlet多线程机制,通过一个实例并结合Java 的内存模型说明引起Servlet线程安全的原因,给出了保证Servlet线程安全的三种解决方案,并说明三种方案在实际开发中的取舍。    Servlet/JSP技术和...

    摘 要:介绍了Servlet多线程机制,通过一个实例并结合Java 的内存模型说明引起Servlet线程不安全的原因,给出了保证Servlet线程安全的三种解决方案,并说明三种方案在实际开发中的取舍。

     

       Servlet/JSP技术和ASP、PHP等相比,由于其多线程运行而具有很高的执行效率。由于Servlet/JSP默认是以多线程模式执行的,所 以,在编写代码时需要非常细致地考虑多线程的安全性问题。然而,很多人编写Servlet/JSP程序时并没有注意到多线程安全性的问题,这往往造成编写 的程序在少量用户访问时没有任何问题,而在并发用户上升到一定值时,就会经常出现一些莫明其妙的问题。

     

      Servlet的多线程机制
      
      Servlet体系结构是建立在Java多线程机制之上的,它的生命周期是由Web容器负责的。当客户端第一次请求某个Servlet时,Servlet容器将会根据web.xml配置文件实例化这个Servlet类。当有新的客户端请求该Servlet时,一般不会再实例化该Servlet类,也就是有多个线程在使用这个实例。Servlet容器会自动使用线程池等技术来支持系统的运行,如图1所示。

                                            

      这样,当两个或多个线程同时访问同一个Servlet时,可能会发生多个线程同时访问同一资源的情况,数据可能会变得不一致。所以在用Servlet构建的Web应用时如果不注意线程安全的问题,会使所写的Servlet程序有难以发现的错误。

      Servlet的线程安全问题

      Servlet的线程安全问题主要是由于实例变量使用不当而引起的,这里以一个现实的例子来说明。

    复制代码

     1 public class ConcurrentTest extends HttpServlet {
     2     PrintWriter output;
     3     @Override
     4     protected void service(HttpServletRequest request, HttpServletResponse response)
     5             throws ServletException, IOException {
     6         String  username;
     7         response.setContentType("text/html;charset=gb2312");
     8         username=request.getParameter("username");
     9         output=response.getWriter();
    10         try {
    11             //为了突出并发问题,在这设置一个延时
    12             Thread.sleep(5000);
    13             output.println("用户名:"+username+"<BR>"); 
    14         } catch (Exception e) {
    15             e.printStackTrace();
    16         }
    17     }
    18 }

    复制代码

      该Servlet中定义了一个实例变量output,在service方法将其赋值为用户的输出。当一个用户访问该Servlet时,程序会正常的运行,但当多个用户并发访问时,就可能会出现其它用户的信息显示在另外一些用户的浏览器上的问题。这是一个严重的问题。为了突出并发问题,便于测试、观察,我们在回显用户信息时执行了一个延时的操作。假设已在web.xml配置文件中注册了该Servlet,现有两个用户a和b同时访问该Servlet(可以启动两个IE浏览器,或者在两台机器上同时访问),即同时在浏览器中输入:

      a: http://localhost:8080/ServletTest/ConcurrentTest?Username=a
      b: http://localhost:8080/ServletTest/ConcurrentTest?Username=b

      如果用户b比用户a回车的时间稍慢一点,将得到如图2所示的输出:

      

                                            

                                                              图2 a用户和b用户的浏览器输出

      从图2中可以看到,Web服务器启动了两个线程分别处理来自用户a和用户b的请求,但是在用户a的浏览器上却得到一个空白的屏幕,用户a的信息显示在用户b的浏览器上。该Servlet存在线程不安全问题。下面我们就从分析该实例的内存模型入手,观察不同时刻实例变量output的值来分析使该Servlet线程不安全的原因。

      Java的内存模型JMM(Java Memory Model)JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有实例变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存由缓存和堆栈两部分组成,缓存中保存的是主存中变量的拷贝,缓存可能并不总和主存同步,也就是缓存中变量的修改可能没有立刻写到主存中;堆栈中保存的是线程的局部变量,线程之间无法相互直接访问堆栈中的变量。根据JMM,我们可以将论文中所讨论的Servlet实例的内存模型抽象为图3所示的模型。

                                         

      下面根据图3所示的内存模型,来分析当用户a和b的线程(简称为a线程、b线程)并发执行时,Servlet实例中所涉及变量的变化情况及线程的执行情况,如图4所示。

                

      从图4中可以清楚的看到,由于b线程对实例变量output的修改覆盖了a线程对实例变量output的修改,从而导致了用户a的信息显示在了用户b的浏览器上。如果在a线程执行输出语句时,b线程对output的修改还没有刷新到主存,那么将不会出现图2所示的输出结果,因此这只是一种偶然现象,但这更增加了程序潜在的危险性。

       设计线程安全的Servlet

      通过上面的分析,我们知道了实例变量不正确的使用是造成Servlet线程不安全的主要原因。下面针对该问题给出了三种解决方案并对方案的选取给出了一些参考性的建议。

      1、实现 SingleThreadModel 接口

      该接口指定了系统如何处理对同一个Servlet的调用。如果一个Servlet被这个接口指定,那么在这个Servlet中的service方法将不会有两个线程被同时执行,当然也就不存在线程安全的问题。这种方法只要将前面的Concurrent Test类的类头定义更改为:

    1 public class ConcurrentTest extends HttpServlet implements SingleThreadModel  {
    2       ...  ...      
    3 }

      javax.servlet.SingleThreadModel API及其翻译

      Ensures that servlets handle only one request at a time. This interface has no methods.

      确保servlet每次只处理一项请求。接口不含方法。

      If a servlet implements this interface, you are guaranteed that no two threads will execute concurrently in the servlet's service method. The servlet container can make this guarantee by synchronizing access to a single instance of the servlet, or by maintaining a pool of servlet instances and dispatching each new request to a free servlet.

      如果servlet实现了该接口,会确保不会有两个线程同时执行servlet的service方法。 servlet容器通过同步化访问servlet的单实例来保证,也可以通过维持servlet的实例池,对于新的请求会分配给一个空闲的servlet。

      Note that SingleThreadModel does not solve all thread safety issues. For example, session attributes and static variables can still be accessed by multiple requests on multiple threads at the same time, even when SingleThreadModel servlets are used. It is recommended that a developer take other means to resolve those issues instead of implementing this interface, such as avoiding the usage of an instance variable or synchronizing the block of the code accessing those resources. This interface is deprecated in Servlet API version 2.4.

      注意:SingleThreadModel不会解决所有的线程安全隐患。 例如,会话属性和静态变量仍然可以被多线程的多请求同时访问,即便使用了SingleThreadModel servlet。建议开发人员应当采取其他手段来解决这些问题,而不是实现该接口,比如 避免实例变量的使用或者在访问资源时同步代码块。该接口在Servlet API 2.4中将不推荐使用。

       2、同步对共享数据的操作

      使用synchronized 关键字能保证一次只有一个线程可以访问被保护的区段,在本论文中的Servlet可以通过同步块操作来保证线程的安全。同步后的代码如下:

    复制代码

     1 public class ConcurrentTest extends HttpServlet {
     2     PrintWriter output;
     3     @Override
     4     protected void service(HttpServletRequest request, HttpServletResponse response)
     5             throws ServletException, IOException {
     6         String  username;
     7         response.setContentType("text/html;charset=gb2312");
     8         username=request.getParameter("username");
     9         synchronized(this){
    10             output=response.getWriter();
    11             try {
    12                 //为了突出并发问题,在这设置一个延时
    13                 Thread.sleep(5000);
    14                 output.println("用户名:"+username+"<BR>"); 
    15             } catch (Exception e) {
    16                 e.printStackTrace();
    17             }
    18         }
    19     }
    20 }

    复制代码

      

      3、避免使用实例变量

      本实例中的线程安全问题是由实例变量造成的,只要在Servlet里面的任何方法里面都不使用实例变量,那么该Servlet就是线程安全的。

      修正上面的Servlet代码,将实例变量改为局部变量实现同样的功能,代码如下:

    复制代码

     1 public class ConcurrentTest extends HttpServlet {
     2     @Override
     3     protected void service(HttpServletRequest request, HttpServletResponse response)
     4             throws ServletException, IOException {
     5         PrintWriter output;
     6         String username;
     7         response.setContentType("text/html;charset=gb2312");
     8         username=request.getParameter("username");
     9         synchronized(this){
    10             output=response.getWriter();
    11             try {
    12                 //为了突出并发问题,在这设置一个延时
    13                 Thread.sleep(5000);
    14                 output.println("用户名:"+username+"<BR>"); 
    15             } catch (Exception e) {
    16                 e.printStackTrace();
    17             }
    18         }
    19     }
    20 }

    复制代码

      对上面的三种方法进行测试,可以表明用它们都能设计出线程安全的Servlet程序。但是,如果一个Servlet实现了SingleThreadModel接口,Servlet引擎将为每个新的请求创建一个单独的Servlet实例,这将引起大量的系统开销。SingleThreadModel在Servlet2.4中已不再提倡使用;同样如果在程序中使用同步来保护要使用的共享的数据,也会使系统的性能大大下降。这是因为被同步的代码块在同一时刻只能有一个线程执行它,使得其同时处理客户请求的吞吐量降低,而且很多客户处于阻塞状态。另外为保证主存内容和线程的工作内存中的数据的一致性,要频繁地刷新缓存,这也会大大地影响系统的性能。所以在实际的开发中也应避免或最小化 Servlet 中的同步代码;在Serlet中避免使用实例变量是保证Servlet线程安全的最佳选择。从Java 内存模型也可以知道,方法中的临时变量是在栈上分配空间,而且每个线程都有自己私有的栈空间,所以它们不会影响线程的安全。

      小结

      Servlet的线程安全问题只有在大量的并发访问时才会显现出来,并且很难发现,因此在编写Servlet程序时要特别注意。线程安全问题主要是由实例变量造成的,因此在Servlet中应避免使用实例变量。如果应用程序设计无法避免使用实例变量,那么使用同步来保护要使用的实例变量,但为保证系统的最佳性能,应该同步可用性最小的代码路径。

    展开全文
  • Servlet规范定义,在默认情况下(Servlet不是在分布式的环境...答案是采用多线程Servlet容器维护了一个多线程池来服务请求。线程池实际上是等待执行代码的一组线程,这些线程叫做工作者线程(Worker Thread)。Serv
    Servlet规范定义,在默认情况下(Servlet不是在分布式的环境中部署),Servlet容器对声明的每一个Servlet,只创建
    一个实例。如果有多个客户请求同时访问这个Servlet,Serv
    let容器如何处理这多个请求呢?答案是采用多
    线程,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中,要对外部对象(例如,文件)进行修改操作,一定要加锁,做到互斥的访问。
    展开全文
  • Servlet 线程安全问题Servlet 中使用的是多线程方式来执行 service()方法处理请求,所以我们在使用 Servlet 时需要考虑到线程安全问题,在多线程中对于对象中的成员变量是最不安全的,所以不要在 Servlet 中...

    Servlet 线程安全问题

    • 在 Servlet 中使用的是多线程方式来执行 service()方法处理请求,所以我们在使用 Servlet 时需要考虑到线程安全问题,在多线程中对于对象中的成员变量是最不安全的,所以不要在 Servlet 中通过成员变量的方式来存放数据,如果一定要使用成员变量存储数据,在对数据进行操作时需要使用线程同步的方式来解决线程安全问题,避免出现数据张冠李戴现象
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    /**
     * Servlet的线程安全
     */
    public class ThreadSafeServlet extends HttpServlet {
        private PrintWriter pw;
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            this.doPost(req, resp);
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    
            String name = req.getParameter("name");
            //synchronized (this){
               pw = resp.getWriter();
               try{
                   Thread.sleep(5000);
                   pw.println(name);
                   pw.flush();
                   pw.close();
               }catch(Exception e){
                   e.printStackTrace();
               }
           //}
        }
    }
    
        <servlet>
            <servlet-name>threadSafeServlet</servlet-name>
            <servlet-class>com.servlet.ThreadSafeServlet</servlet-class>
        </servlet>
        <servlet-mapping>
            <servlet-name>threadSafeServlet</servlet-name>
            <url-pattern>/safe.do</url-pattern>
        </servlet-mapping>
    

    运行结果:
    结果
    结果
    实现数据同步的话就添加synchronized (this)即可
    也就是放开代码的注释

    展开全文
  • Servlet线程安全问题总结

    千次阅读 2017-07-08 09:48:29
    servlet线程安全

    原出处:http://www.cnblogs.com/gw811/archive/2012/09/07/2674859.html ( 加以修改)

    概述

     Servlet/JSP技术和ASP、PHP等相比,由于其多线程运行而具有很高的执行效率。由于Servlet/JSP默认是以多线程模式执行的,所 以,在编写代码时需要非常细致地考虑多线程的安全性问题。然而,很多人编写Servlet/JSP程序时并没有注意到多线程安全性的问题,这往往造成编写 的程序在少量用户访问时没有任何问题,而在并发用户上升到一定值时,就会经常出现一些莫明其妙的问题。

    引出问题:

    当多个客户端并发访问同一个Servlet时,web服务器会为每一个客户端的访问请求创建一个线程,并在这个线程上调用Servlet的service方法,因此service方法内如果访问了同一个资源的话,就有可能引发线程安全问题。

    解决问题

    1、如果某个Servlet实现了SingleThreadModel接口,那么Servlet引擎将以单线程模式来调用其service方法。但是本质上面并没有解决线程安全问题,因为其采用的方式是根据并发请求去产生多个Servlet实例对象,并发的每个线程分别调用一个独立的Servlet实例对象,而真正意义上解决多线程安全问题是指一个Servlet实例对象被多个线程同时调用的问题。而不是多个线程调用多个Servlet实例。
    2、利用synchronized同步代码,将我们有并发问题的代码转化为同步代码,但是这样做的一个坏处就是,如果遇到高并发的情况下,我们的用户可能会因为同步代码的问题而造成等待的时间比较久这样的问题,所以,要慎用。
    3、减少引起并发问题的资源代码,从根本上解决Servlet线程安全问题,其实,造成Servlet线程的问题好多都是因为变量设置不正确造成的,我们应该尽可能去避免这样的问题。

    总结

    总结1:servlet是线程不安全的,而造成这种原因主要是以为实例变量不正确的使用
    总结2 :尽量不使用synchronized来解决Servlet线程安全问题

    预备知识

    Servlet运行过程:
    Servlet程序是由WEB服务器调用,web服务器收到客户端的Servlet访问请求后:
    ① Web服务器首先检查是否已经装载并创建了该Servlet的实例对象。如果是,则直接执行第④步,否则,执行第②步。
    ② 装载并创建该Servlet的一个实例对象。
    ③ 调用Servlet实例对象的init()方法。
    ④ 创建一个用于封装HTTP请求消息的HttpServletRequest对象和一个代表HTTP响应消息的HttpServletResponse对象,然后调用Servlet的service()方法并将请求和响应对象作为参数传递进去。
    ⑤WEB应用程序被停止或重新启动之前,Servlet引擎将卸载Servlet,并在卸载之前调用Servlet的destroy()方法。

    通过第二步,我们可以知道,创建的Servlet对象只有一个。

    引录

    ————————————————- 华丽的分隔符 —————————————————-

    Servlet/JSP技术和ASP、PHP等相比,由于其多线程运行而具有很高的执行效率。由于Servlet/JSP默认是以多线程模式执行的,所 以,在编写代码时需要非常细致地考虑多线程的安全性问题。然而,很多人编写Servlet/JSP程序时并没有注意到多线程安全性的问题,这往往造成编写 的程序在少量用户访问时没有任何问题,而在并发用户上升到一定值时,就会经常出现一些莫明其妙的问题。

    Servlet的多线程机制

    Servlet体系结构是建立在Java多线程机制之上的,它的生命周期是由Web容器负责的。当客户端第一次请求某个Servlet时,Servlet容器将会根据web.xml配置文件实例化这个Servlet类。当有新的客户端请求该Servlet时,一般不会再实例化该Servlet类,也就是有多个线程在使用这个实例。Servlet容器会自动使用线程池等技术来支持系统的运行,如图1所示。
    这里写图片描述

    这样,当两个或多个线程同时访问同一个Servlet时,可能会发生多个线程同时访问同一资源的情况,数据可能会变得不一致。所以在用Servlet构建的Web应用时如果不注意线程安全的问题,会使所写的Servlet程序有难以发现的错误。

    Servlet的线程安全问题

     Servlet的线程安全问题主要是由于实例变量使用不当而引起的,这里以一个现实的例子来说明。

    public class ConcurrentTest extends HttpServlet {
        PrintWriter output;
        @Override
        protected void service(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            String  username;
            response.setContentType("text/html;charset=gb2312");
            username=request.getParameter("username");
            output=response.getWriter();  // output注意 声明位置
            try {
                //为了突出并发问题,在这设置一个延时
                Thread.sleep(5000);
                output.println("用户名:"+username+"<BR>"); 
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

     该Servlet中定义了一个实例变量output,在service方法将其赋值为用户的输出。当一个用户访问该Servlet时,程序会正常的运行,但当多个用户并发访问时,就可能会出现其它用户的信息显示在另外一些用户的浏览器上的问题。这是一个严重的问题。为了突出并发问题,便于测试、观察,我们在回显用户信息时执行了一个延时的操作。假设已在web.xml配置文件中注册了该Servlet,现有两个用户a和b同时访问该Servlet(可以启动两个IE浏览器,或者在两台机器上同时访问),即同时在浏览器中输入:
      a: http://localhost:8080/ServletTest/ConcurrentTest?Username=a
      b: http://localhost:8080/ServletTest/ConcurrentTest?Username=b

      如果用户b比用户a回车的时间稍慢一点,将得到如图2所示的输出:
      这里写图片描述
    从图2中可以看到,Web服务器启动了两个线程分别处理来自用户a和用户b的请求,但是在用户a的浏览器上却得到一个空白的屏幕,用户a的信息显示在用户b的浏览器上。该Servlet存在线程不安全问题。下面我们就从分析该实例的内存模型入手,观察不同时刻实例变量output的值来分析使该Servlet线程不安全的原因。

    Java的内存模型JMM(Java Memory Model)JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有实例变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存由缓存和堆栈两部分组成,缓存中保存的是主存中变量的拷贝,缓存可能并不总和主存同步,也就是缓存中变量的修改可能没有立刻写到主存中;堆栈中保存的是线程的局部变量,线程之间无法相互直接访问堆栈中的变量。根据JMM,我们可以将论文中所讨论的Servlet实例的内存模型抽象为图3所示的模型。
    这里写图片描述

    下面根据图3所示的内存模型,来分析当用户a和b的线程(简称为a线程、b线程)并发执行时,Servlet实例中所涉及变量的变化情况及线程的执行情况,如图4所示
    这里写图片描述
    从图4中可以清楚的看到,由于b线程对实例变量output的修改覆盖了a线程对实例变量output的修改,从而导致了用户a的信息显示在了用户b的浏览器上。如果在a线程执行输出语句时,b线程对output的修改还没有刷新到主存,那么将不会出现图2所示的输出结果,因此这只是一种偶然现象,但这更增加了程序潜在的危险性。
    设计线程安全的Servlet

      通过上面的分析,我们知道了实例变量不正确的使用是造成Servlet线程不安全的主要原因。下面针对该问题给出了三种解决方案并对方案的选取给出了一些参考性的建议。

      1、实现 SingleThreadModel 接口 ( 过时 )

      该接口指定了系统如何处理对同一个Servlet的调用。如果一个Servlet被这个接口指定,那么在这个Servlet中的service方法将不会有两个线程被同时执行,当然也就不存在线程安全的问题。这种方法只要将前面的Concurrent Test类的类头定义更改为:
      

    public class ConcurrentTest extends HttpServlet implements SingleThreadModel  {
          ...  ...      
     }

    javax.servlet.SingleThreadModel API及其翻译

      Ensures that servlets handle only one request at a time. This interface has no methods.

      确保servlet每次只处理一项请求。接口不含方法。

      If a servlet implements this interface, you are guaranteed that no two threads will execute concurrently in the servlet’s service method. The servlet container can make this guarantee by synchronizing access to a single instance of the servlet, or by maintaining a pool of servlet instances and dispatching each new request to a free servlet.

      如果servlet实现了该接口,会确保不会有两个线程同时执行servlet的service方法。 servlet容器通过同步化访问servlet的单实例来保证,也可以通过维持servlet的实例池,对于新的请求会分配给一个空闲的servlet。

      Note that SingleThreadModel does not solve all thread safety issues. For example, session attributes and static variables can still be accessed by multiple requests on multiple threads at the same time, even when SingleThreadModel servlets are used. It is recommended that a developer take other means to resolve those issues instead of implementing this interface, such as avoiding the usage of an instance variable or synchronizing the block of the code accessing those resources. This interface is deprecated in Servlet API version 2.4.

      注意:SingleThreadModel不会解决所有的线程安全隐患。 例如,会话属性和静态变量仍然可以被多线程的多请求同时访问,即便使用了SingleThreadModel servlet。建议开发人员应当采取其他手段来解决这些问题,而不是实现该接口,比如 避免实例变量的使用或者在访问资源时同步代码块。该接口在Servlet API 2.4中将不推荐使用。

      2、同步对共享数据的操作 ( 慎用 )

      使用synchronized 关键字能保证一次只有一个线程可以访问被保护的区段,在本论文中的Servlet可以通过同步块操作来保证线程的安全。同步后的代码如下:

    public class ConcurrentTest extends HttpServlet {
        PrintWriter output;
        @Override
        protected void service(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            String  username;
            response.setContentType("text/html;charset=gb2312");
            username=request.getParameter("username");
            synchronized(this){
                output=response.getWriter();
                try {
                    //为了突出并发问题,在这设置一个延时
                    Thread.sleep(5000);
                    output.println("用户名:"+username+"<BR>"); 
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    3、避免使用实例变量( 推荐 )

      本实例中的线程安全问题是由实例变量造成的,只要在Servlet里面的任何方法里面都不使用实例变量,那么该Servlet就是线程安全的。

      修正上面的Servlet代码,将实例变量改为局部变量实现同样的功能,代码如下:
      

    public class ConcurrentTest extends HttpServlet {
        @Override
        protected void service(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            PrintWriter output;
            String username;
            response.setContentType("text/html;charset=gb2312");
            username=request.getParameter("username");
            synchronized(this){
                output=response.getWriter();
                try {
                    //为了突出并发问题,在这设置一个延时
                    Thread.sleep(5000);
                    output.println("用户名:"+username+"<BR>"); 
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

     对上面的三种方法进行测试,可以表明用它们都能设计出线程安全的Servlet程序。但是,如果一个Servlet实现了SingleThreadModel接口,Servlet引擎将为每个新的请求创建一个单独的Servlet实例,这将引起大量的系统开销。SingleThreadModel在Servlet2.4中已不再提倡使用;同样如果在程序中使用同步来保护要使用的共享的数据,也会使系统的性能大大下降。这是因为被同步的代码块在同一时刻只能有一个线程执行它,使得其同时处理客户请求的吞吐量降低,而且很多客户处于阻塞状态。另外为保证主存内容和线程的工作内存中的数据的一致性,要频繁地刷新缓存,这也会大大地影响系统的性能。所以在实际的开发中也应避免或最小化 Servlet 中的同步代码;在Serlet中避免使用实例变量是保证Servlet线程安全的最佳选择。从Java 内存模型也可以知道,方法中的临时变量是在栈上分配空间,而且每个线程都有自己私有的栈空间,所以它们不会影响线程的安全。

    ———————————————- 华丽的分隔符 —————————————————-

    另一测试用例(自己的)

    public class ServletDemo extends HttpServlet {
    
    
        //子类不能抛比父类更多的异常
        int i = 1;
        public void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
                i++;   //  全部变量的 i
                try {
                    Thread.sleep(1000*4);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                response.getWriter().write(i+"");
            }
    
        public void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
    
            doGet(request, response);
        }
    
    }

    这里写图片描述

    说明:
    在上述代码中,由于 i 变量的设置,在我们打开浏览器进行访问时,我们利用线程来模拟并发,当我们的第一个用户通过浏览器发送一个请求进行访问我们的这个Servlet时,然后迅速点击另外一个浏览器也进行访问相同的访问,而此时我们的意愿是第一个用户访问在页面上的显示应该是2,第二个访问的用户是3,但是,此时我们的浏览器的显示都为3,这也就是我们所说的Servlet 线程 不安全问题。当我们在代码中利用synchronized代码块进行包裹时如下图:

    public class ServletDemo extends HttpServlet {
    
        int i = 1;
        public void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
    
            synchronized (this) {   //锁旗标  锁    
                i++; 
                try {
                    Thread.sleep(1000*4);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                response.getWriter().write(i+"");
            }
        }
    
        public void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
    
            doGet(request, response);
        }
    
    }
    

    这里写图片描述

    展开全文
  • Servlet不是线程安全的。 要解释为什么Servlet为什么不是线程安全的,需要了解Servlet容器(即Tomcat)使如何响应HTTP请求的。 当Tomcat接收到Client的HTTP请求时,Tomcat从线程池中取出一个线程,之后找到该请求...
  • Servlet单例多线程详解

    2015-08-24 21:22:17
    1.Servlet的单例特点; 2.Servlet的可用变量分析; 3.Servlet如何处理多个请求; 4.如何开发线程安全Servlet; 5.Struts1、Struts2、Spring 对应的Action或Controller比较。
  • Servlet线程安全性解析

    2009-11-02 17:46:49
    线程不安全,因为被多个线程访问一个实例,共享的数据可能会发生安全问题。  每个客户端访问Servlet时,都是一个独立的线程。  概述 在探讨java线程安全前,让我们先简要介绍一下Java语言。 任何语言,如C++,C#...
  • 1、Servlet的 XML文件配置的一点细节 1)一个Servlet程序,可以配置多个<url-pattern>元素,表示一个Servlet有多个资源名称,必须使用 / 打头 2)一个Servlet程序,可以配置多个<servlet-mapping>...
  • Servlet是单例多线程的无需置疑;...线程安全问题详解:多个线程之间是不能直接传递数据交互的,它们之间的交互只能通过共享变量来实现。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有实例变量...
  • Java Servlet单例多线程机制详解

    千次阅读 2016-11-15 13:58:53
    改了一下原来的名字Java Servlet线程问题为Java Servlet单例多线程机制详解。 因为我觉得这样说比较贴切。 一直想不通Java Servlet既然在服务器中是单例存在,而他的service方法又不是sychronized,而且他是多...
  • 本文详细介绍了Servlet生命周期以及Servlet线程安全问题
  • response线程安全详解

    2018-08-15 23:42:23
    本文主要分析 HttpServletResponse 注入的线程安全问题 问 HttpServletResponse 是不是线程安全? 这个答案要针对具体的场景才能说明,接下来用3个场景演示 HttpServletResponse 是不是线程安全这个疑问 方法注入 ...
  • 以前那种静态网页已经不再是适应,我们看到今天的网页不但有flash,vide等等,显然以前的那种静态网页展示无法解决,为了解决这个问题,SUN公司提供了一门用于解决上述出现的问题的技术,这就是Servlet技术。...
  • Servlet 3.0 新特性详解

    千次阅读 2014-07-01 14:19:16
    Servlet 3.0 新特性概述 Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布。该版本在前一版本(Servlet 2.5)的基础上提供了若干...1、异步处理支持:有了该特性,Servlet 线程不再需要一直阻
  • servlet问题详解

    2014-09-24 07:53:56
    ervlet是javaee技术当中很...这里笔者罗列了一些常见的跟servlet相关的面试问题与解答,希望能够帮助大家: 1.web服务器(web server)和应用服务器(appliction server)的区别是什么? web服务器的任务是处理客
  • Servlet详解

    2018-03-27 11:19:58
    Servlet的部署(1) 由于客户端是通过URL地址访问web服务器中的资源,所以Servlet程序若想被外界访问,必须把servlet程序映射到一个URL地址上,这个工作在web.xml文件中使用&lt;servlet&gt;元素和&lt;...
  • servlet作为java web开发举足轻重的东西,在此总结一下对Servlet的学习(jsp也是servlet) 一。servlet生命周期 加载—>实例化—>服务—>销毁 加载由web服务器容器完成。 init():在Servlet的生命周期中,仅...
  • 摘自《Servlet JSP深入详解 基于Tomcat的Web开发》
  • servlet详解

    2017-08-13 10:23:09
    servlet详解
  • Servlet 运行工作原理详解 事实上,servlet就是一个Java接口,interface! 打开idea,ctrl + shift + n,搜索servlet,就可以看到是一个只有5个方法的interface! [外链图片转存失败,源站可能有防盗链机制,建议将图片...
  • Servlet 是 Java EE 规范体系的重要组成部分,也是 Java 开发人员必须具备的基础技能,Servlet 3.0 是 Servlet 规范的最新版本。本文主要介绍了 Servlet 3.0 引入的若干重要新特性,包括异步处理、新增的注解支持、...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 15,024
精华内容 6,009
关键字:

servlet线程安全问题的详解