Servlet 线程安全

Servlet/JSP技術和ASP、PHP等相比,由於其多線程運行而具有很高的執行效率。由於Servlet/JSP默認是以多線程模式執行的,所以,在編寫代碼時需要非常細緻地考慮多線程的安全性問題。然而,很多人編寫Servlet/JSP程式時並沒有注意到多線程安全性的問題,這往往造成編寫的程式在少量用戶訪問時沒有任何問題,而在併發用戶上升到一定值時,就會經常出現一些莫明其妙的問題。
  
   Servlet的多線程機制
  
  Servlet體系結構是建立在Java多線程機制之上的,它的生命週期是由Web容器負責的。當客戶端第一次請求某個Servlet時,Servlet容器將會根據web.xml配置文件實例化這個Servlet類。當有新的客戶端請求該Servlet時,一般不會再實例化該Servlet類,也就是有多個線程在使用這個實例。Servlet容器會自動使用線程池等技術來支援系統的運行,如圖1所示。
  
 

  
圖1 Servlet線程池

  
  這樣,當兩個或多個線程同時訪問同一個Servlet時,可能會發生多個線程同時訪問同一資源的情況,數據可能會變得不一致。所以在用Servlet構建的Web應用時如果不注意線程安全的問題,會使所寫的Servlet程式有難以發現的錯誤。
  
   Servlet的線程安全問題
  
  Servlet的線程安全問題主要是由於實例變數使用不當而引起的,這裡以一個現實的例子來說明。
  
  Import javax.servlet. *;
  Import javax.servlet.http. *;
  Import java.io. *;
  Public class Concurrent Test extends HttpServlet {PrintWriter output;
  Public void service (HttpServletRequest request,
  HttpServletResponse response) throws ServletException, IOException {String username;
  Response.setContentType ("text/html; charset=gb2312");
  Username = request.getParameter ("username");
  Output = response.getWriter ();
  Try {Thread. sleep (5000); //為了突出併發問題,在這設置一個延時
  } Catch (Interrupted Exception e){}
  output.println("用戶名:"+Username+"<BR>");
  }
  }
  
  該Servlet中定義了一個實例變數output,在service方法將其賦值為用戶的輸出。當一個用戶訪問該Servlet時,程式會正常的運行,但當多個用戶併發訪問時,就可能會出現其他用戶的資訊顯示在另外一些用戶的瀏覽器上的問題。這是一個嚴重的問題。為了突出併發問題,便於測試、觀察,我們在回顯用戶資訊時執行了一個延時的操作。假設已在web.xml配置文件中註冊了該Servlet,現有兩個用戶a和b同時訪問該Servlet(可以啟動兩個IE瀏覽器,或者在兩台機器上同時訪問),即同時在瀏覽器中輸入:
  
  a: http://localhost: 8080/servlet/ConcurrentTest? Username=a
  
  b: http://localhost: 8080/servlet/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 Servlet實例的JMM模型

  
  下面根據圖3所示的記憶體模型,來分析當用戶a和b的線程(簡稱為a線程、b線程)併發執行時,Servlet實例中所涉及變數的變化情況及線程的執行情況,如圖4所示。
  

  
圖4 Servlet實例的線程調度情況


  
  從圖4中可以清楚的看到,由於b線程對實例變數output的修改覆蓋了a線程對實例變數output的修改,從而導致了用戶a的資訊顯示在了用戶b的瀏覽器上。如果在a線程執行輸出語句時,b線程對output的修改還沒有刷新到主存,那麼將不會出現圖2所示的輸出結果,因此這只是一種偶然現象,但這更增加了程式潛在的危險性。 

解决办法

先判断你的servlet是线程安全的吗?


Are you thread-safe?
Below is a simple servlet that is not thread-safe. Look closely, because at first glance, nothing appears wrong with it:

package threadSafety;

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.*;
import java.math.*;

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

The variable counter is an instance variable, called such because it is tied to the class instance. Because it is defined within the class definition, it belongs within that class instance. It's convenient to place our variables within this scope because it lives outside each of the class's methods and can be accessed at any time. The value is also retained between method calls. The problem here is that our servlet container is multithreaded and shares single instances of servlets for multiple requests. Does defining your variables as instance variables sound like a good idea now? Remember, only one place in memory is allocated for this variable, and it is shared between all threads that intend on executing this same class instance.

Let's find out what happens when we execute this servlet simultaneously. We add a delay in processing by using the sleep() method. This method helps simulate more accurate behavior, as most requests differ in the amount of time required for processing. Of course, as is our luck as programmers, this also causes our problem to occur more often. This simple servlet will increment counter such that each servlet should be able to display sequential values. We create simultaneous requests by using HTML frames; each frame's source is the same servlet:

<HTML>
  <BODY>
    <TABLE>
      <TR>
        <TD>
          <IFRAME src="/theWebapp/SimpleServlet"
                  name="servlet1"
                  height="200%">
          </IFRAME>
        </TD>
      </TR>
      <TR>
        <TD>
          <IFRAME src="/theWebapp/SimpleServlet"
                  name="servlet2"
                  height="200%">
          </IFRAME>
        </TD>
      </TR>
      <TR>
        <TD>
          <IFRAME src="/theWebapp/SimpleServlet"
                  name="servlet3"
                  height="200%">
          </IFRAME>
        </TD>
      </TR>
    </TABLE>
  </BODY>
</HTML>

Our code, which is a non-thread-safe servlet, generates the following output:

ThreadSafety.SimpleServlet@1694eca:
Counter=0
Counter=2
Counter=4
Counter=6
Counter=9
Counter=11
Counter=13
Counter=15
Counter=17
Counter=19

ThreadSafety.SimpleServlet@1694eca:
Counter=0
Counter=1
Counter=3
Counter=5
Counter=7
Counter=8
Counter=10
Counter=12
Counter=14
Counter=16

ThreadSafety.SimpleServlet@1694eca:
Counter=18
Counter=20
Counter=22
Counter=23
Counter=24
Counter=25
Counter=26
Counter=27
Counter=28
Counter=29

As we can see in our output, we fail to get the results we desire. Notice the value printed from the this reference is duplicated. This is the servlet's memory address. It tells us that only one servlet is instantiated to service all requests. The servlet tried its best to output sequential data, but because all threads share the memory allocated for counter, we managed to step on our own toes. We can see that the values are not always sequential, which is bad! What if that variable is being used to point at a user's private information? What if a user logs into their online banking system and on a particular page, that user sees someone else's banking information? This problem can manifest itself in many ways, most of which are difficult to identify, but the good news is that this problem is easily remedied. So let's take a look at our options.

Your first defense: Avoidance
I have always said that the best way to fix problems is to avoid them all together; in our case, this approach is best. When discussing thread safety, we are interested only in the variables that we both read and write to and that pertain to a particular Web conversation. If the variable is for read-only use or it is application-wide, then no harm results in sharing this memory space across all instances. For all other variable uses, we want to make sure that we either have synchronized access to the variable (more on this in a moment) or that we have a unique variable for each thread.

To ensure we have our own unique variable instance for each thread, we simply move the declaration of the variable from within the class to within the method using it. We have now changed our variable from an instance variable to a local variable. The difference is that, for each call to the method, a new variable is created; therefore, each thread has its own variable. Before, when the variable was an instance variable, the variable was shared for all threads processing that class instance. The following thread-safe code has a subtle, yet important, difference. Notice where the counter variable is declared!

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.*;
import java.math.*;

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

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

Move the variable declaration to within the doGet() method and test again. Notice a change in behavior? I know, you're thinking; "It can't be that easy," but usually it is. As you scramble to revisit your latest servlet code to check where you declared your variables, you may run into a small snag. As you move your variables from within the class definition to within the method, you may find that you were leveraging the scope of the variable and accessing it from within other methods. If you find yourself in this situation, you have a couple of choices. First, change the method interfaces and pass this variable (and any other shared variables) to each method requiring it. I highly recommend this approach. Explicitly passing your data elements from method to method is always best; it clarifies your intentions, documents each method's requirements, makes your code well structured, and offers many other benefits.

If you discover that you must share a variable between servlets and this variable is going to be read from and written to by multiple threads (and you are not storing it in a database), then you will require thread synchronization. Sorry, there is no way around it now.

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值