自由飞翔

我在仰望,java之上

统计

留言簿(2)

我关注的blog

阅读排行榜

评论排行榜

JSP、servlet、struts线程安全问题分析

Servlet之所以比CGI效率高就是因为Servlet是多线程的。
Servlet规范已经声明Servlet不是线程安全的.
JSP中使用声明的变量是Servlet的实例变量,不是线程安全的,其他都是线程安全的。

那么怎样才能是Servlet安全呢,凡是多个线程可以共享的就不要使用(实例变量+类变量),就这么简单。也可以使用synchronized同步方法,但是这样效率不高,还可以使用单线程模型,这样的话效率就更低了,100个请求同时来的时候就要实例化100个实例。

方法中的临时变量是不会影响线程安全的,因为他们是在栈上分配空间,而且每个线程都有自己私有的栈空间。

总结:线程安全问题主要是由实例变量造成的,不管在Servlet还是JSP,或者在Struts的Action里面,不要使用实例变量,
任何方法里面都不要出现实例变量,你的程序就是线程安全的。


在Servlet/JSP中的几种变量类型
源自 : http://www.javaresearch.org/article/8465.htm
一、在Servlet/JSP中的几种变量类型
在编写Servlet/JSP程序时,对实例变量一定要小心使用。因为实例变量是非线程安全的。
在Servlet/JSP中,变量可以归为下面的几类:
1. 类变量
request,response,session,config,application,以及JSP页面内置的page, pageContext。
其中除了application外,其它都是线程安全的。
2. 实例变量
实例变量是实例所有的,在堆中分配。在Servlet/JSP容器中,一般仅实例化一个Servlet/JSP实例,
启动多个该实例的线程来处理请求。而实例变量是该实例所有的线程所共享,所以,实例变量不是线程安全的。
3. 局部变量
局部变量在堆栈中分配,因为每一个线程有自己的执行堆栈,所以,局部变量是线程安全的。
二、在Servlet/JSP中的多线程同步问题
在JSP中,使用实例变量要特别谨慎。首先请看下面的代码:
// instanceconcurrenttest.jsp
<%@ page contentType="text/html;charset=GBK" %>
<%!
    //定义实例变量
    String username;
    String password;
    java.io.PrintWriter output;
%>
<%
    //从request中获取参数
    username = request.getParameter("username");
    password = request.getParameter("password");
    output = response.getWriter();
    showUserInfo();    
%>
<%!
    public void showUserInfo() {
        //为了突出并发问题,在这儿首先执行一个费时操作
        int i =0;
        double sum = 0.0;
        while (i++ < 200000000) {
            sum += i;
        }
        
        output.println(Thread.currentThread().getName() + "<br>");
        output.println("username:" + username + "<br>");
        output.println("password:" + password + "<br>");
    }
%>
在这个页面中,首先定义了两个实例变量,username和password。
然后在从request中获取这两个参数,并调用showUserInfo()方法将请求用户的信息回显在该客户的浏览器上。
在一个用户访问是,不存在问题。
但在多个用户并发访问时,就会出现其它用户的信息显示在另外一些用户的浏览器上的问题。这是一个严重的问题。
为了突出并发问题,便于测试、观察,我们在回显用户信息时执行了一个模拟的费时操作,
比如,下面的两个用户同时访问(可以启动两个IE浏览器,或者在两台机器上同时访问):
a:    http://localhost:8080/instanceconcurrenttest.jsp?username=a&password=123
b:    http://localhost:8080/instanceconcurrenttest.jsp?username=b&password=456
如果a点击链接后,b再点击链接,那么,a将返回一个空白屏幕,b则得到a以及b两个线程的输出。请看下面的屏幕截图:
 
从运行结果的截图上可以看到,Web服务器启动了两个线程分别来处理来自a和b的请求,但是在a却得到一个空白的屏幕。这是因为上面程序中的output, username和password都是实例变量,是所有线程共享的。在a访问该页面后,将output设置为a的输出,username,password分别置为a的信息,而在a执行printUserInfo()输出username和password信息前,b又访问了该页面,把username和password置为了b的信息,并把输出output指向到了b。随后a的线程打印时,就打印到了b的屏幕了,并且,a的用户名和密码也被b的取代。请参加下图所示:
而实际程序中,由于设置实例变量,使用实例变量这两个时间点非常接近,
所以,像本例的同步问题并没有这么突出,可能会偶尔出现,但这却更加具有危险性,也更加难于调试。
同样,对于Servlet也存在实例变量的多线程问题,请看上面页面的Servlet版:
// InstanceConcurrentTest.java
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.PrintWriter;
public class InstanceConcurrentTest extends HttpServlet 
{
    String username;
    String password;
    PrintWriter out;
    public void doGet(HttpServletRequest request, HttpServletResponse response) 
         throws ServletException,java.io.IOException
    {
        //从request中获取参数
        username = request.getParameter("username");
        password = request.getParameter("password");
        System.out.println(Thread.currentThread().getName() + 
                     " | set username:" + username);
        out = response.getWriter();
        showUserInfo();    
    }
    public void showUserInfo() {
        //为了突出并发问题,在这儿首先执行一个费时操作
        int i =0;
        double sum = 0.0;
        while (i++ < 200000000) {
            sum += i;
        }
        out.println("thread:" + Thread.currentThread().getName());
        out.println("username:"+ username);
        out.println("password:" + password);
    }
}
三、解决方案
1. 以单线程运行Servlet/JSP
在JSP中,通过设置:<%@ page isThreadSafe="false" %>,在Servlet中,
通过实现javax.servlet.SingleThreadModel,
此时Web容器将保证JSP或Servlet实例以单线程方式运行。
重要提示:在测试中发现,Tomcat 4.1.17不能正确支持isThreadSafe属性,
所以,指定isTheadSafe为false后,在Tomcat 4.1.17中仍然出现多线程问题,这是Tomcat 4.1.17的Bug。在Tomcat 3.3.1和Resin 2.1.5中测试通过。
2. 去除实例变量,通过参数传递
从上面的分析可见,应该在Servlet/JSP中尽量避免使用实例变量。
比如,下面的修正代码,去除了实例变量,通过定义局部变量,并参数进行传递。
这样,由于局部变量是在线程的堆栈中进行分配的,所以是线程安全的。
不会出现多线程同步的问题。代码如下:
<%@ page contentType="text/html;charset=GBK" %>
<%
    //使用局部变量
    String username;
    String password;
    java.io.PrintWriter output;
    //从request中获取参数
    username = request.getParameter("username");
    password = request.getParameter("password");
    output = response.getWriter();
    showUserInfo(output, username, password);    
%>
<%!
    public void showUserInfo(java.io.PrintWriter _output, 
         String _username, String _password) {
        //为了突出并发问题,在这儿首先执行一个费时操作
        int i =0;
        double sum = 0.0;
        while (i++ < 200000000) {
            sum += i;
        }
        
        _output.println(Thread.currentThread().getName() + "<br>");
        _output.println("username:" + _username + "<br>");
        _output.println("password:" + _password + "<br>");
    }
%>
注:有的资料上指出在printUserInfo()方法或者实例变量的相关操作语句上使用synchronized关键字进行同步,
但这样并不能解决多线程的问题。因为,这样虽然可以使对实例变量的操作代码进行同步,
但并不能阻止一个线程使用另外一个线程修改后的“脏的”实例变量。
所以,除了降低运行效率外,不会起到预期效果。


待续...............


Gavin

posted on 2011-09-02 10:30 GavinMiao 阅读(1060) 评论(0)  编辑  收藏 所属分类: corejava


只有注册用户登录后才能发表评论。


网站导航: