`

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:

public class SimpleServlet extends HttpServlet
{
       // A variable that is NOT thread-safe!
       private int counter = 0;

       public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
       {
              doPost(req, resp);
       }

       public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
       {
              resp.getWriter().println("<HTML><BODY>");
              resp.getWriter().println(this + " ==> ");
              resp.getWriter().println(Thread.currentThread() + ": <br>");
              for (int c = 0; c < 10; c++)
              {
                     resp.getWriter().println("Counter = " + counter + "<BR>");
                     try
                     {
                            Thread.sleep((long) Math.random() * 1000);
                            counter++;
                     }
                     catch (InterruptedException exc)
                     {
                     }
              }
              resp.getWriter().println("</BODY></HTML>");
       }
}


然后,我们通过一个html页面向该servlet发出三次请求:
<HTML>
<BODY>
<TABLE>
    <TR>
        <TD><IFRAME src="./SimpleServlet" name="servlet1" height="200%"> </IFRAME></TD>
    </TR>
    <TR>
        <TD><IFRAME src="./SimpleServlet" name="servlet2" height="200%"> </IFRAME></TD>
    </TR>
    <TR>
        <TD><IFRAME src="./SimpleServlet" name="servlet3" height="200%"> </IFRAME></TD>
    </TR>
</TABLE>
</BODY>
</HTML>


刷新页面几次后,产生的结果为:

com.zwchen.servlet.SimpleServlet@11e1bbf ==> Thread[http-8081-Processor23,5,main]:
Counter = 60
Counter = 61
Counter = 62
Counter = 65
Counter = 68
Counter = 71
Counter = 74
Counter = 77
Counter = 80
Counter = 83

com.zwchen.servlet.SimpleServlet@11e1bbf ==> Thread[http-8081-Processor22,5,main]:
Counter = 61
Counter = 63
Counter = 66
Counter = 69
Counter = 72
Counter = 75
Counter = 78
Counter = 81
Counter = 84
Counter = 87

com.zwchen.servlet.SimpleServlet@11e1bbf ==> Thread[http-8081-Processor24,5,main]:
Counter = 61
Counter = 64
Counter = 67
Counter = 70
Counter = 73
Counter = 76
Counter = 79
Counter = 82
Counter = 85
Counter = 88

我们会发现三点:
servlet只产生了一个Servlet对象,因为输出this时,其hashcode都一样,
servlet在不同的线程(线程池)中运行,如http-8081-Processor22,http-8081-Processor23
Count被这三个doGet方法共享,并且并行修改。

上面的结果,违反了线程安全的两个方面。
那么,我们怎样保证按照我们期望的结果运行呢?首先,我想保证产生的count都是顺序执行的。
我们将Servlet代码重构如下:
public class SimpleServlet extends HttpServlet
{
  //A variable that is NOT thread-safe!
  private int counter = 0;
  private String mutex = "";

  public void doGet(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException
  {
    doPost(req, resp);
  }

  public void doPost(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException
  {
    resp.getWriter().println("<HTML><BODY>");
    resp.getWriter().println(this + ": <br>");
    synchronized (mutex)
    {   

      for (int c = 0; c < 10; c++)
      {
        resp.getWriter().println("Counter = " + counter + "<BR>");
        try
          {
          Thread.sleep((long) Math.random() * 1000);
          counter++;
        }
        catch (InterruptedException exc) { }
      }
    }
    resp.getWriter().println("</BODY></HTML>");
  }
}


我们的输出结果为:
com.zwchen.servlet.SimpleServlet@109da93:
Counter = 0
Counter = 1
Counter = 2
Counter = 3
Counter = 4
Counter = 5
Counter = 6
Counter = 7
Counter = 8
Counter = 9

com.zwchen.servlet.SimpleServlet@109da93:
Counter = 10
Counter = 11
Counter = 12
Counter = 13
Counter = 14
Counter = 15
Counter = 16
Counter = 17
Counter = 18
Counter = 19

com.zwchen.servlet.SimpleServlet@109da93:
Counter = 20
Counter = 21
Counter = 22
Counter = 23
Counter = 24
Counter = 25
Counter = 26
Counter = 27
Counter = 28
Counter = 29


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

现在,我们保证了顺序,但是我们怎么保证Counter字段(不是局部变量!)在每个Servlet的线程下都是独立的呢?也就是说,并发请求时,它们都不相互干扰。
我现在将Servlet代码重构如下:
public class SimpleServlet extends HttpServlet
{
       private ThreadLocal counter = new ThreadLocal() {
              protected synchronized Object initialValue()
              {
                     return new Integer(0);
              }
       };

       public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
       {
              doPost(req, resp);
       }

       public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
       {
              resp.getWriter().println("<HTML><BODY>");
              resp.getWriter().println(this + "[" + Thread.currentThread() + "]: <br>");
              for (int c = 0; c < 10; c++)
              {

                     resp.getWriter().println(counter.get() + "<br>");
                     try
                     {
                            Thread.sleep((long) Math.random() * 1000);
                            int c1 = ((Integer)counter.get()).intValue();
                            c1++;
                            counter.set(new Integer(c1));
                     }
                     catch (InterruptedException exc)
                     {
                     }

              }
              resp.getWriter().println("</BODY></HTML>");
       }
}

现在,我刷新html页面三次,第三次结果如下:
com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor22,5,main]]:
20
21
22
23
24
25
26
27
28
29
com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor25,5,main]]:
20
21
22
23
24
25
26
27
28
29
com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor23,5,main]]:
20
21
22
23
24
25
26
27
28
29


从以上结果,我们可以发现:
1、  在该html页面内的并发三次请求中,该Servlet里面的counter字段都不相互干扰
2、  counter字段还是实例字段,并且都会保留状态,不是每次都用0开始
3、  html页面内的三次请求都在不同的线程,但在同一个实例中。
总之,在Java里面,字段(不是局部变量)有三个共享范围:instance field,static field,local thread field,而后者往往在服务器端这种多线程环境必须考虑到的。
     
在J2EE项目开发过程中,ThreadLocal类有时有非常重要的作用,下面是我碰到的,但可以延伸:
1、           在用Hibernate做web开发的持久化时,有个模式叫做Open Session In View,也就是将session保留到页面中,在response结束后,在OpenSessionInViewFilter中关闭session,这对于延迟加载非常有效,例如,我们在页面上显示User的详细信息,需要显示该user的所属Department的信息; 但是,在list users这种不需要显示department信息的地方,那个user的department信息就不会加载,也就是说加载相关信息是动态的,但不会出现LazyInitializationException,也就是Load on demand。不过,注意慎用该模式。
2、           在工作流开发,例如OSWorflow,每次调用其服务前,都需要将caller对象传入,这样会导致我们的方法非常臃肿,如果我们在调用该方法的上层,如在Servlet里调用它之前,将User对象置于ThreadLocal中,那么可以在工作流方法内通过get()方法获取,而不用传入参数。
3、           为什么Web框架中,Webwork的action中可以有field,但Struts却不能?其实,也就是说,Struts不是线程安全的,而Webwork是线程安全的。大家可以参考Webwork的ActionContext类:
public class ActionContext implements Serializable {
static ThreadLocal actionContext = new ActionContextThreadLocal();
……………..
而对于Struts,我们可以从ActionServlet.process() => RequestProcessor. processActionPerform,在RequestProcessor中有字段  protected

分享到:
评论

相关推荐

    servlet线程安全问题

    Servlet 线程安全问题 Servlet 线程安全问题是指在使用 Servlet 编程时,如果不注意多线程安全性问题,可能会导致难以发现的错误。Servlet/JSP 技术由于其多线程运行而具有很高的执行效率,但这也意味着需要非常...

    Servlet线程安全问题.docx

    Servlet线程安全问题详解 在Java Web开发中,Servlet是一个重要的组件,用于处理HTTP请求。然而,Servlet在多线程环境下的线程安全问题是一个不容忽视的话题。在Servlet的生命周期中,Tomcat容器会根据需求实例化...

    深入研究Servlet线程安全性问题.pdf

    #### 三、Servlet线程安全问题分析 线程安全问题是由于Servlet中实例变量的使用不当导致的。在多线程环境中,如果多个线程同时修改相同的实例变量,则可能导致数据不一致性问题。例如,在上面给出的`ConcurrentTest...

    Servlet线程安全的解决方法

    #### 一、理解Servlet线程安全问题 Servlet是Java平台上的一个重要的Web组件模型,用于处理HTTP请求并返回响应。每当客户端向服务器发送请求时,服务器会创建一个线程来处理这个请求。由于Servlet是单例模式实现的...

    深入研究Servlet线程安全性问题

    ### Servlet线程安全问题实例分析 以一个简单的Servlet为例,该Servlet接收用户请求,读取参数,并输出结果: ```java public class Test extends HttpServlet { private static final long serialVersionUID = 1...

    servlet与Struts action线程安全问题分析

    Servlet和Struts Action是两种常见的Java Web开发组件,它们在多线程环境下运行时可能存在线程安全问题。线程安全是指在多线程环境中,一个类或者方法能够正确处理多个线程的并发访问,保证数据的一致性和完整性。 ...

    servlet与Struts action线程安全问题分析(pdf)

    #### 三、Servlet线程安全问题 线程安全问题主要发生在Servlet中使用实例变量时。当多个线程并发访问同一个Servlet实例时,如果这些线程试图同时修改实例变量,就可能发生数据不一致的情况。以下通过一个具体的示例...

    Servlet网上售票问题引发线程安全问题的思考

    在讨论了Servlet线程安全问题的代码实现后,让我们来总结一下Servlet线程安全问题的几个要点: 1. 线程安全问题产生的原因:在多用户环境下,多个线程同时访问和修改共享资源(如Servlet中的`ticket`变量)时,如果...

    Servlet是线程不安全的1

    Servlet是一种线程不安全的组件,它的线程不安全性体现在多线程环境下共享一个实例变量,导致线程安全问题。下面我们将从Servlet的工作原理说起,详细解释Servlet接收和响应客户请求的过程,并探讨Servlet线程不安全...

    Servlet线程同步计数器实验

    Servlet线程同步计数器实验是Java Web开发中一个重要的实践课题,主要涉及到Servlet容器如何处理并发请求以及如何确保在多线程环境下的数据一致性。在这个实验中,我们将深入理解Servlet生命周期、线程模型以及线程...

    servlet多线程

    4. **避免在Servlet中创建额外线程**:由于Servlet本身已具备多线程特性,额外创建线程可能会引入复杂的线程安全问题,除非有特殊需求。 5. **对外部资源的线程安全操作**:在多个Servlet中对同一外部资源(如文件...

    1工作临时-servlet 多线程问题

    在这个场景下,可能的问题可能是由于Servlet在处理高并发请求时,由于多线程并发访问共享资源导致的线程安全问题。 在Java Web环境中,Servlet容器(如Tomcat、Jetty等)会为每个HTTP请求创建一个新的线程来处理,...

    韩顺平Servlet学习源代码

    5. **Servlet线程安全问题**: Servlet容器通常会为每个请求创建一个新的线程,因此,Servlet实例默认是线程安全的。但是,如果在Servlet中定义了成员变量,需要考虑多线程环境下可能产生的并发问题。 6. **...

Global site tag (gtag.js) - Google Analytics