解决servlet非线程安全所导致jsp传参异常的一种方案

解决servlet非线程安全所导致jsp传参错误的一种方案
由于servlet是个线程池,而且是非线程安全,所以当压力过大的时候,容易导致一些奇怪的现象。
如下面的代码

 // 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两个线程的输出




如果是简单的将showUserInfo方法简单的synchronized并不能解决问题。(可以试一下)。

解决问题的办法是给方法中的变量传一个实例进取。

  <%!
//定义实例变量
String username;
String password;
java.io.PrintWriter output;
%>
<%
//从request中获取参数
username = request.getParameter("username");
password = request.getParameter("password");
output = response.getWriter();
showUserInfo(username,password,output);
//showUserInfo();
%>
<%!
public void showUserInfo(String user ,String pass,java.io.PrintWriter _output) {
//	public void showUserInfo() {
//为了突出并发问题,在这儿首先执行一个费时操作
int i =0;
double sum = 0.0;
while (i++ < 200000000) {
	sum += i;
}
_output.println(Thread.currentThread().getName() + "<br>");
_output.println("username:" + user + "<br>");
_output.println("password:" + pass + "<br>");
}
%>

这样就可以解决问题。

但我有点想不明白的是java传参数是传得引用,可是这里如果是引用的话那和上面的代码效果应该一样。所以推断,传得是实例。
问题时解决了,但我不想继续得过且过,不知道我那里想错了,请各位指点。

上面的例子摘自:http://publish.it168.com/2005/1209/20051209002201_hezuo.shtml?cChanNel=11&cpositioncode=296&hezuo=2

1 楼 JAVA_ED 2007-09-03  
你前面一种方法可以compile过吗
这里的uname,pwd.. 都是jspservice方法里的栈变量
不传param在别的method里应该取不到的
2 楼 bluepopopo 2007-09-03  
确认第二种方法可行?定义为实例变量会有线程不安全,这与与是否引用无关
3 楼 likenice 2007-09-04  
赫赫。想通了。
先在这里回答上面两位的问题。
1。代码绝对可以运行。
2。导致第一种问题(servlet线程池问题)的根源绝对是引用。

给大家提示下我们正常中一般给一个方法传个对象参数后,一般是在方法外面的到然后再处理。而对这个方法外面参数对象基本不会附值。引用(指针)方向就不会变。
今天忙。没时间说在清除。大家想想吧。很有意思。(和我们正常的class编码习惯不同)
4 楼 likenice 2007-09-04  
忘说了。如果说代码无法运行是由于美一行代码前面有隐藏的空格(此空格需要删除。这个是拷贝到网页上面的通病,所以要注意。)
5 楼 xianyun 2007-09-04  
 
不明白楼主为什么要定义静态变量
知道什么是静态变量吗?<%!和<%的区别吗?

另外,第二种方法你可以把延时放到前面这里再试一下

//为了突出并发问题,在这儿首先执行一个费时操作  
int i =0;  
double sum = 0.0;  
while (i++ < 200000000) {  
    sum += i;  
}
showUserInfo(username,password,output);  
6 楼 likenice 2007-09-04  
回楼上的。
我明白什么是全局变量。
如果您觉得可以不用全局变量,jsp中的方法内部怎么调用参数。(jsp的方法是全局的。而且必须是全局的)
当然这么写本身就是不推荐的。但我是看别人的东西引发的一些想法。重点不在我的前提条件怎么设置,而是结论。我的结论是错误的(我认为java参数是传得引用,原来认为:如果是引用则不能达到预期的效果。可实际中达到了。^.^!!!!很绕口,不知道说清楚了没。)
大家如果对代码由疑问,请测试一下。
7 楼 flash 2007-09-05  
xianyun 写道
 
不明白楼主为什么要定义静态变量
知道什么是静态变量吗?<%!和<%的区别吗?

另外,第二种方法你可以把延时放到前面这里再试一下

//为了突出并发问题,在这儿首先执行一个费时操作  
int i =0;  
double sum = 0.0;  
while (i++ < 200000000) {  
    sum += i;  
}
showUserInfo(username,password,output);  

<%!%>之内的并不是静态变量 只是一个实例变量。
8 楼 flash 2007-09-05  
其实这个问题很容易理解,主要是从两点来理解,
一是servlet的实例池;
另一个是java的参数传递方式。
servlet是被容器实例化后放入实例池中的,每一个servlet会有一个或多个实例,来处理针对该servlet的请求。因此,在高并发情况下,有可能会出现一个实例被多个请求调用的情况。
就拿楼主的第一个例子来讲,A用户在请求这个servlet后,调用了servlet实例池中的x实例,然后处理请求,当代码执行到“//为了突出并发问题,在这儿首先执行一个费时操作”这里时(此时实例x的实例变量username="userA"),刚好B用户也发出一个请求,刚好同样调用 了实例池中的x实例,然后将username修改为"userB"。然后也进入“//为了突出并发问题,在这儿首先执行一个费时操作”。正常情况下,应该是A的请球进程先执行完”这个费时操作“(其实哪个先后都无所谓了)。这时A的请求进程中输出username,肯定是“userB"。
在楼主的第二个例子为什么给方法加入参数就可以呢,这就是java方法的参数传递方式影响的。通常来讲,java 传递对象时是引用传递,传递基本类型时是值 传递。但用一种严格的说法,java传递参数的方式都是值传递,也就是传递的全是一个副本。只不过对对象而言,这个副本是该对像的一个引用副本。所以通常大家都讲,对象是引用传递。如果按照这种通常的说法,我们还应该把String类型的对象看成是值传递而不是引用传递,为什么?请大家看下面这个例子
public class StringTest {
static	String  aaa="aaa";
public static void main (String[] args){
	test(aaa);
}
public static void test(String a){
	aaa="bbb";
	System.out.println(a);
}
}

aaa的初始值是"aaa",被传入到test方法后,aaa的值被改写为"bbb"。最后输入a的值,仍然是"aaa"。
其实这个就是楼主提出问题的一个概括,只是放在一个进程中实现的罢了。在aaa变量 被传入test方法时,会创建一个aaa的引用副本,这个引用副本指向的是"aaa"这个对象。此时,有两个引用都指向"aaa"对象,他们分别是aaa和a。现在你再去给aaa赋值,只是把aaa的引用指向一个新的对象"bbb",而对"aaa"对象并任何影响。当然,jvm会不定时检测,如果"aaa"没有被任何引用指向,将会回收他的内存空间。但是在此时,在 test方法的运行栈里还有一个a引用来指向"aaa",所以"aaa"这个对象的不会被回收。现在的情况已经很明了,在内存就是有两个引用,aaa和a,他们分别指向"bbb"对象和"aaa"对象。这个情况是不是很像基本类型的值传递?为什么会这样,个人感觉应该是String的赋值方式给大家带来的错觉吧。String aaa="aaa",其实就是String aaa=new String("aaa");你重新给aaa赋值,就是重新创建了一个对象,然后指向他,感觉就像是对一个副本进行操作,而对原版没有影响一样。
呵呵,说的啰嗦了点,这方面可以看一下java传递相关研究的文章,比我说的清晰多了。

最后,再来分析楼主的第二个例子,就很容易理解了
由于在A请求中,调用showUserInfo(String user ,String pass,java.io.PrintWriter _output)方法时,内存中已经有user这个引用指向了对象"userA",那么当B请求来修改username时,只不过是把 username指向了一个新对象"userB"。然后在A请求中输出user的值 ,那肯定是它指向的"userA"了,与uaername已经完全没有关系了。
9 楼 likenice 2007-09-05  
flash 写道
其实这个问题很容易理解,主要是从两点来理解,
一是servlet的实例池;
另一个是java的参数传递方式。
servlet是被容器实例化后放入实例池中的,每一个servlet会有一个或多个实例,来处理针对该servlet的请求。因此,在高并发情况下,有可能会出现一个实例被多个请求调用的情况。
就拿楼主的第一个例子来讲,A用户在请求这个servlet后,调用了servlet实例池中的x实例,然后处理请求,当代码执行到“//为了突出并发问题,在这儿首先执行一个费时操作”这里时(此时实例x的实例变量username="userA"),刚好B用户也发出一个请求,刚好同样调用 了实例池中的x实例,然后将username修改为"userB"。然后也进入“//为了突出并发问题,在这儿首先执行一个费时操作”。正常情况下,应该是A的请球进程先执行完”这个费时操作“(其实哪个先后都无所谓了)。这时A的请求进程中输出username,肯定是“userB"。
在楼主的第二个例子为什么给方法加入参数就可以呢,这就是java方法的参数传递方式影响的。通常来讲,java 传递对象时是引用传递,传递基本类型时是值 传递。但用一种严格的说法,java传递参数的方式都是值传递,也就是传递的全是一个副本。只不过对对象而言,这个副本是该对像的一个引用副本。所以通常大家都讲,对象是引用传递。如果按照这种通常的说法,我们还应该把String类型的对象看成是值传递而不是引用传递,为什么?请大家看下面这个例子
public class StringTest {
static	String  aaa="aaa";
public static void main (String[] args){
	test(aaa);
}
public static void test(String a){
	aaa="bbb";
	System.out.println(a);
}
}

aaa的初始值是"aaa",被传入到test方法后,aaa的值被改写为"bbb"。最后输入a的值,仍然是"aaa"。
其实这个就是楼主提出问题的一个概括,只是放在一个进程中实现的罢了。在aaa变量 被传入test方法时,会创建一个aaa的引用副本,这个引用副本指向的是"aaa"这个对象。此时,有两个引用都指向"aaa"对象,他们分别是aaa和a。现在你再去给aaa赋值,只是把aaa的引用指向一个新的对象"bbb",而对"aaa"对象并任何影响。当然,jvm会不定时检测,如果"aaa"没有被任何引用指向,将会回收他的内存空间。但是在此时,在 test方法的运行栈里还有一个a引用来指向"aaa",所以"aaa"这个对象的不会被回收。现在的情况已经很明了,在内存就是有两个引用,aaa和a,他们分别指向"bbb"对象和"aaa"对象。这个情况是不是很像基本类型的值传递?为什么会这样,个人感觉应该是String的赋值方式给大家带来的错觉吧。String aaa="aaa",其实就是String aaa=new String("aaa");你重新给aaa赋值,就是重新创建了一个对象,然后指向他,感觉就像是对一个副本进行操作,而对原版没有影响一样。
呵呵,说的啰嗦了点,这方面可以看一下java传递相关研究的文章,比我说的清晰多了。

最后,再来分析楼主的第二个例子,就很容易理解了
由于在A请求中,调用showUserInfo(String user ,String pass,java.io.PrintWriter _output)方法时,内存中已经有user这个引用指向了对象"userA",那么当B请求来修改username时,只不过是把 username指向了一个新对象"userB"。然后在A请求中输出user的值 ,那肯定是它指向的"userA"了,与uaername已经完全没有关系了。
呵呵,精辟。
我想了半天不知道怎么表达。
(可能跟本人有点懒有关系吧。本来想画个图,结果画了几个不满意。)
在这里只能说楼上的辛苦了。