java知识总结(8)

介绍了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中应避免使用实例变量。如果应用程序设计无法避免使用实例变量,那么使用同步来保护要使用的实例变量,但为保证系统的最佳性能,应该同步可用性最小的代码路径。




在探讨java线程安全前,让我们先简要介绍一下Java语言。

任何语言,如C++,C#,Java,它们都有相通之处,特别是语法,但如果有人问你,Java语言的核心是什么?类库?关键字?语法?似乎都不是。Java语言的核心,也就是Sun始终不愿意开源的东西:Java虚拟机的实现(不过sun公开了其Java虚拟机规范),也就有了BEA的JRockit,IBM的Jikes,Sun的Hotspot。

Java的核心有两点,Java类加载(Java Class Loader)和Java内存管理,它们具体体现在Java类库的以下几个类:

java.lang.ClassLoader(java.lang.Class):我们调用的类,包括其接口和超类,import的类是怎么被Java虚拟机载入的?为什么static的字段在servlet容器里面可以一直生存下去(Spring容器中)?

java.lang.Thread(java.lang.ThreadLocal):垃圾回收是怎么进行的(垃圾回收线程)?我们的程序是怎么退出的?

java.lang.refelect.Proxy(java.lang.refelect.Method):为什么Tomcat、Tapestry、Webwork、Spring等容器和框架可以通过配置文件来调用我们写的类?Servlet规范、JSF规范、EJB规范、JDBC规范究竟是怎么回事?为什么它们几乎都是一些接口,而不是具体类?


Servlet线程安全

在Java的server side开发过程中,线程安全(Thread Safe)是一个尤为突出的问题。因为容器,如Servlet、EJB等一般都是多线程运行的。虽然在开发过程中,我们一般不考虑这些问题,但诊断问题(Robust),程序优化(Performance),我们必须深入它们。

什么是线程安全?

引用
Thread-safe describes a program portion or routine that can be called from multiple programming threads without unwanted interaction between the threads。



在Java里,线程安全一般体现在两个方面:

  1、多个thread对同一个java实例的访问(read和modify)不会相互干扰,它主要体现在关键字synchronized。如ArrayList和Vector,HashMap和Hashtable(后者每个方法前都有synchronized关键字)。如果你在interator一个List对象时,其它线程remove一个element,问题就出现了。

  2、每个线程都有自己的字段,而不会在多个线程之间共享。它主要体现在java.lang.ThreadLocal类,而没有Java关键字支持,如像static、transient那样。


一个普遍的疑问,我们的Servlet中能够像JavaBean那样declare instance或static字段吗?如果不可以?会引发什么问题?

答案是:不可以。我们下面以实例讲解:

首先,我们写一个普通的Servlet,里面有instance字段count:

  web.xml  >>

复制代码
1 <servlet>
2     <servlet-name>SimpleServlet</servlet-name>
3     <servlet-class>servlet.SimpleServlet</servlet-class>
4 </servlet>
5 <servlet-mapping>
6     <servlet-name>SimpleServlet</servlet-name>
7     <url-pattern>/SimpleServlet</url-pattern>
8 </servlet-mapping>
复制代码

  SimpleServlet  >>

复制代码
 1 public class SimpleServlet extends HttpServlet {
 2      private int counter = 0;  
 3      @Override
 4     protected void service(HttpServletRequest request, HttpServletResponse response)
 5             throws ServletException, IOException {
 6         response.getWriter().println("<HTML><BODY>");
 7         response.getWriter().println(this + " ==> ");
 8         response.getWriter().println(Thread.currentThread() + ": <br>"); 
 9         for(int c=0;c<10;c++){
10             response.getWriter().println("Counter = " + counter + "<BR>");
11             try {
12                 Thread.sleep(1000);  
13                 counter++;  
14             } catch (Exception e) {
15                 e.printStackTrace();
16             }
17         }
18         response.getWriter().println("</BODY></HTML>");
19     }
20 }
复制代码

  test.html  >>

复制代码
1 <HTML>  
2     <BODY>  
3         <TABLE>  
4             <TR>  
5                 <TD><IFRAME src="SimpleServlet" name="servlet1" height="200%"> </IFRAME></TD>  
6             </TR>  
7         </TABLE>  
8     </BODY>  
9 </HTML>  
复制代码

   大家应该发现,test.html写的和zwchen的博客原文中的写的有点区别,本来也是按照zwchen的博客原文中的去测试的,但是相信很多人并没有得出理想的结果,正如博客下面评论上5楼所说的:“没有出现线程安全问题,数字的顺序都是正确的”,我也是如此(我用的是Firefox浏览器)。后来换了IE浏览器进行测试出现下面的问题,在页面上只显示出了第一个<tr></tr>里面的内容,于是我的处理方法就是:test.html的内容如上所示,打开3个IE浏览器,同时在浏览器中输入:

  a: http://localhost:8080/ServletTest/SimpleServlet
  b: http://localhost:8080/ServletTest/SimpleServlet

 

  c: http://localhost:8080/ServletTest/SimpleServlet 

   测试结果如下:

        

   我们会发现三点:

  1、Servlet是一个单例对象(Singleton),因为我们看到多次请求的this指针所有打印出来的hashCode值都相同。
  2、servlet在不同的线程(线程池)中运行,如http-8080-1,http-8080-2,http-8080-3等输出值可以明显区分出不同的线程执行了不同一段Servlet逻辑代码。
  3、count变量在不同的线程中共享,而且它的值被不同的线程修改,输出时已经不是顺序输出。也就是说,其他的线程会篡改当前线程中实例变量的值,针对这些对象的访问不是线程安全的。


  上面的结果,违反了线程安全的两个方面。

  那么,我们怎样保证按照我们期望的结果运行呢?首先,我想保证产生的count都是顺序执行的。
  我们将Servlet代码重构如下:

 

复制代码
 1 public class SimpleServlet extends HttpServlet {
 2      private int counter = 0;  
 3      private String mutex = ""; 
 4      @Override
 5     protected void service(HttpServletRequest request, HttpServletResponse response)
 6             throws ServletException, IOException {
 7         response.getWriter().println("<HTML><BODY>");
 8         response.getWriter().println(this + " ==> ");
 9         response.getWriter().println(Thread.currentThread() + ": <br>"); 
10         synchronized (mutex){
11             for(int c=0;c<10;c++){
12                 response.getWriter().println("Counter = " + counter + "<BR>");
13                 try {
14                     Thread.sleep(1000);  
15                     counter++;  
16                 } catch (Exception e) {
17                     e.printStackTrace();
18                 }
19             }
20         }
21         response.getWriter().println("</BODY></HTML>");
22     }
23 }
复制代码

  这符合了我们的要求,输出都是按顺序的,这正式synchronized的含义。

  附带说一下,我现在synchronized的是一个字符串变量mutex,不是this对象,这主要是从performance和 Scalability考虑。Synchronized用在this对象上,会带来严重的可伸缩性的问题(Scalability),所有的并发请求都要排队!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值