在之前的博文中我们讲了如何新建和运行一个Servlet,也讲了一般的Servlet的知识点,如通配符映射等。
那么思考下,我们要访问一个写好了的Servlet,单独访问是没有问题的,如果有n个用户同时访问这个网址呢?会出现什么问题?
public class ConcurrentTest extends HttpServlet {
/**
*
*/
private static final long serialVersionUID = -2153241422283937673L;
PrintWriter output;
@Override
protected 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(10000);
output.println("用户名:"+username+"<BR>");
} catch (Exception e) {
e.printStackTrace();
}
}
编辑如上代码,当每次访问时都输出当前访问对象的username。但是当我们使用不同的浏览器同时访问时,错误出现了:
在同一个用户操作的浏览器上连续输出了两个username,虽然都为null,但是这也是不安全的。
怎么造成的呢?
这是因为在其中一个用户操作的时候,其他用户也可以访问servlet对象,当第二个用户还在操作的时候第一个用户操作完成,这就造成了原本是输出到b用户浏览器上的数据被a用户获取。这是很典型的无法同步线程的读操作,在我之前的JVM里面多线程访问内存数据也有讲到,当时讲的是有两种解决办法。
Servlet为了解决同步问题,才去的思路与JVM是很类似的。
首先第一种方法是:给被访问的对象加锁,途径是实现SingleThreadModel,即单线程模式。
来看下SingleThreadModel的官方定义和解释:
也就是说当Servlet实现了这个方法,那么程序保证在当前只能有一个此Servlet的对象在运行,也就不存在读的竞争关系。但是这个方法并没有完全解决所有的线程问题,比如说session的attribute以及静态变量仍然可以被多请求获得,获得同一时间的多个线程获得。也就是说虽然这个对象被保证了同一时间只能有一个,但是session的attribute是共享的,静态变量存在方法区,也是可以被获得并且可能存在安全读取的隐患的(必然)。所以这个接口也不再被建议使用。
第二种方式是:同步共享数据(加锁)
对要被获取的对象加锁,保证同一时间只能有一个线程访问这个对象。关键字Synchronized。
其中的代码就变成了这个样子:
username=request.getParameter("username");
synchronized(this){
output=response.getWriter();
try {
//为了突出并发问题,在这设置一个延时
Thread.sleep(5000);
output.println("用户名:"+username+"<BR>");
} catch (Exception e) {
e.printStackTrace();
把response这个对象加锁,保证在同一时间只能有一个对象能操作response及其操作。
第三种方式是:使用局部变量而不是实例变量
在刚才写的Servlet类中,因为我们使用两个不同的线程去获取request对象(servlet的内置对象),并且打印出每个user的username,所以造成了输出的错误。
如果把request对象写在方法体中,那么就变成:
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username;
PrintWriter output;
运行了几次没有出现错误。
把每个线程的休眠时间增加。
Thread.sleep(10000);
连续运行。
自动提示当前的request对象还没有释放,没有结束。问题解决。
究竟最后的两种方法哪种更好呢?思考一下。
一种是同步我们的要操作的对象;一种是使用局部对象。
个人感觉还是局部对象更胜一筹,因为我们不能每次都确切的知道需要同步的对象,而且需要手动的去添加同步方法,相比之下使用局部对象就方便的多了。
如果程序必须使用全局变量时,我们还有同步方法可以使用,不是吗?总会有办法的。
ps:
类变量是类中独立于方法之外的变量,用static 修饰。 实例变量也是独立于方法之外的变量,没用static修饰,也就是对着对象实例化而初始化。 方法变量时在方法之内声明的变量,生命周期与方法一致。