首页 理论教育Servlet线程安全问题的解决

Servlet线程安全问题的解决

【摘要】:该Servlet 存在线程不安全问题。表3.3Servlet 实例的线程调度情况从表3.3 中可以清楚的看到,由于lisi 线程对实例变量out 的修改覆盖了zhangsan线程对实例变量out 的修改,从而导致了用户zhangsan 的信息显示在了用户lisi 的浏览器上。所以在实际的开发中也应避免或最小化Servlet 中的同步代码;在Serlet 中避免使用实例变量是保证Servlet 线程安全的最佳选择。所以,ServletContext 属性的访问不是线程安全的。

1.变量的线程安全

Servlet 的线程安全问题主要是由于实例变量使用不当而引起的。

例3.6:变量的线程安全

该Servlet 中定义了一个实例变量out,在service()方法中为其赋值。当一个用户访问该Servlet 时,程序会正常地运行,但当多个用户并发访问时,就可能会出现其他用户的信息显示在另外一些用户的浏览器上的问题。这是一个严重的问题。为了突出并发问题,便于测试、观察,在输出用户信息之前执行了一个延时的操作。假设已在web.xml 配置文件中注册了该Servlet,现有两个用户zhangsan 和lisi 同时访问该Servlet(可以启动两个IE 浏览器,或者在两台机器上同时访问),即同时在浏览器地址栏输入。

用户“zhangsan”输入。

用户“lisi”输入:

如果用户lisi 比用户zhangsan 敲回车的时间稍慢一点,将得到如图3.26 所示的输出效果。

图3.26 用户zhangsan 和用户lisi 的浏览器输出

从图3.26 中可以看到,Web 服务器启动了两个线程分别处理来自用户zhangsan 和用户lisi 的请求,但是在用户zhangsan 的浏览器上却得到一个空白的屏幕,用户zhangsan的信息显示在用户lisi 的浏览器上。该Servlet 存在线程不安全问题。下面就从分析该实例的内存模型入手,观察不同时刻实例变量out的值来分析使该Servlet线程不安全的原因。

Java 的内存模型JMM(Java Memory Model)主要是规定了线程和内存之间的一些关系。根据JMM 的设计,系统存在一个主内存(Main Memory),简称为主存,Java 中所有实例变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存由缓存和堆栈两部分组成,缓存中保存的是主存中变量的拷贝,缓存可能并不总和主存同步,也就是缓存中变量的修改可能没有立刻写到主存中;堆栈中保存的是线程的局部变量,线程之间无法相互直接访问堆栈中的变量。根据JMM,可以将课程中所讨论的Servlet 实例的内存模型抽象为图3.27 所示的模型。

图3.27 Servlet 实例的JMM 模型

下面根据图3.27 所示的内存模型,来分析当用户zhangsan 和lisi 的线程(简称为zhangsan 线程、lisi 线程)并发执行时,Servlet 实例中所涉及变量的变化情况及线程的执行情况,见表3.3。

表3.3 Servlet 实例的线程调度情况

从表3.3 中可以清楚的看到,由于lisi 线程对实例变量out 的修改覆盖了zhangsan线程对实例变量out 的修改,从而导致了用户zhangsan 的信息显示在了用户lisi 的浏览器上。如果在zhangsan 线程执行输出语句时,lisi 线程对out 的修改还没有刷新到主存,那么将不会出现如图3.26 所示的输出结果。虽然这只是一种偶然现象,但更增加了程序潜在的危险性。

通过上面的分析,知道了实例变量不正确的使用是造成Servlet 线程不安全的原因。下面针对该问题给出了三种解决方案,并对方案的选取给出了一些参考性的建议。(www.chuimin.cn)

(1)实现SingleThreadModel 接口

javx.servlet.SingleThreadModel 接口没有任何的方法,它是一个标识接口。如果一个Servlet 实现了这个接口,Servlet 容器将保证在一个时刻仅有一个线程可以在给定的Servlet 实例的service()方法中执行,所以也就不存在线程安全问题。

这种方法只要将前面的Security 类的类头定义更改为:

(2)同步对共享数据的操作

使用synchronized 关键字能保证一次只有一个线程可以访问被保护的区段,在这里的Servlet 可以通过同步块操作来保证线程的安全。同步后的代码如下:

(3)避免使用实例变量

例3.6 中的线程安全问题是由实例变量造成的,只要在Servlet 中的任何方法里面都不使用实例变量,那么该Servlet 就是线程安全的。修正上面的Servlet 代码,将实例变量改为局部变量实现同样的功能,代码如下:

对上面的三种方法进行测试,可以表明用它们都能设计出线程安全的Servlet 程序。但是,如果一个Servlet 实现了SingleThreadModel 接口,Servlet 引擎将为每个新的请求创建一个单独的Servlet 实例,这将引起大量的系统开销。SingleThreadModel 在Servlet2.4 中已不再提倡使用;同样如果在程序中使用同步来保护要使用的共享的数据,也会使系统的性能大大下降。这是因为被同步的代码块在同一时刻只能有一个线程执行它,使得其同时处理客户请求的吞吐量降低,而且很多客户处于阻塞状态。另外为保证主存内容和线程的工作内存中的数据的一致性,要频繁地刷新缓存,这也会大大影响系统的性能。所以在实际的开发中也应避免或最小化Servlet 中的同步代码;在Serlet 中避免使用实例变量是保证Servlet 线程安全的最佳选择。从Java 内存模型也可以知道,方法中的临时变量是在栈上分配空间,而且每个线程都有自己私有的栈空间,所以它们不会影响线程的安全。

2.属性的安全

在Servlet 中,可以访问保存到ServletContext、HttpSession 和ServletRequest对象中的属性,这三种对象都提供了getAttribute()和setAttribute()方法用于读取和设置属性。那么这三种不同范围的对象的属性访问为何是线程安全的呢?

➢ ServletContext

ServletContext 对象可以被Web 应用程序中所有的Servlet 访问,多个线程可以同时在Servlet 上下文中设置或者读取属性,这将导致存储数据的不一致。所以,ServletContext 属性的访问不是线程安全的。为了避免出现问题,可以对会被许多程序访问的属性进行同步,或者对其产生一个拷贝。但要注意的是,进行同步将造成性能的瓶颈,而拷贝方式的应用,如果访问量比较大,将导致开销的增加。在实际应用中,应该根据具体情况,采用合理的方式。应该合理地设计系统,在Servlet 上下文中只保存不经常需要修改的数据,而对经常需要修改的数据,则采用另外的方式在多个Servlet 中共享。

➢ HttpSession

HttpSession 对象在用户会话期间存活,它不像ServletContext 对象,可以在Web应用程序的所有线程中被访问,HttpSession 对象只能在处理属于同一个Session 的请求的线程中被访问。可以认为在一个时刻只有一个用户请求,因此,Session 对象的属性访问是线程安全的。然而,事实并非如此。一个用户可以打开多个同属于一个进程的浏览器窗口,在这些窗口中的访问请求,属于同一个Session,为了同时处理多个这样的请求,Servlet 容器会创建多个线程,而在这些线程中,就可以同时访问到Session 对象的属性。要避免这个问题,可以对Session 的访问进行同步。

➢ ServletRequest

因为Servlet 容器对它所接收到的每一个请求,都创建一个新的ServletRequest 对象,所以ServletRequest 对象只在一个线程中被访问。因为只有一个线程服务请求,所以请求对象的属性访问是线程安全的。要注意的是,ServletRequest 对象是作为参数传进Servlet 的service()方法中的,在service()方法的范围内,该请求是有效的,不要试图在service()方法结束后,仍然保存请求对象的引用,如果那样的话,请求对象的行为将是不可预料的。

Servlet 的线程安全问题只有在大量的并发访问时才会显现出来,并且很难发现,因此在编写Servlet 程序时要特别注意。线程安全问题主要是由实例变量造成的,因此在Servlet 中应避免使用实例变量。如果应用程序设计无法避免使用实例变量,那么使用同步来保护要使用的实例变量,但为保证系统的最佳性能,应该同步可用性最少的代码。