Servlet线程安全问题

时间:2022-03-05 20:18:15

Servlet采用单实例多线程方式运行,因此是线程不安全的。默认情况下,非分布式系统,Servlet容器只会维护一个Servlet的实例,当多个请求到达同一个Servlet时,Servlet容器会启动多个线程分配给不同请求来执行同一个Servlet实例中的服务方法。为什么这么做?有效利用JVM允许多个线程访问同一个实例的特性,来提高服务器性能。因为,无论是同步线程对Servlet的调用,还是为每一个线程初始化一个Servlet实例,都会带来巨大的性能问题。这也就是为什么Servlet会存在多线程安全问题。

一个Servlet对应多个URL映射,将会生成多个Servlet实例。如下所示:

Servlet线程安全问题

输出结果:

Servlet线程安全问题

输出结果可以看到映射/demoServlet1/demoServlet2对应Servlet实例是不同的。

结果证明:Servlet将为每一个URL映射生成一个实例;一个Servlet可能存在多个示例,但每一个实例都会对应不同的URL映射。

一、Servlet处理多个请求访问过程

Servlet容器默认是采用单实例多线程的方式处理多个请求的。

1、当web服务器启动的时候(或客户端发送请求到服务器时),Servlet就被加载并实例化(只存在一个Servlet实例);

2、容器初始化化Servlet主要就是读取配置文件(例如tomcat,可以通过servlet.xml的<Connector>设置线程池中线程数目,初始化线程池通过web.xml,初始化每个参数值等等。

3、当请求到达时,Servlet容器通过调度线程(Dispatchaer Thread) 调度它管理下线程池中等待执行的线程(Worker Thread)给请求者;

4、线程执行Servlet的service方法;

5、请求结束,放回线程池,等待被调用;(注意:避免使用实例变量(成员变量),因为如果存在成员变量,可能发生多线程同时访问该资源时,都来操作它,照成数据的不一致,因此产生线程安全问题)

从上面可以看出:

第一:Servlet单实例,减少了产生servlet的开销;

第二:通过线程池来响应多个请求,提高了请求的响应时间;

第三:Servlet容器并不关心到达的Servlet请求访问的是否是同一个Servlet还是另一个Servlet,直接分配给它一个新的线程;如果是同一个Servlet的多个请求,那么Servlet的service方法将在多线程中并发的执行;

第四:每一个请求由ServletRequest对象来接受请求,由ServletResponse对象来响应该请求;

Servlet线程安全问题

当容器收到一个Servlet请求,Dispatcher线程从线程池中选出一个工作组线程,将请求传递给该线程,然后由该线程来执行Servlet的service方法。 当这个线程正在执行的时候,容器收到另一个请求,调度者线程将从线程池中选出另外一个工作组线程来服务则个新的请求,容器并不关心这个请求是否访问的是同一个Servlet还是另一个 Servlet。当容器收到对同一个Servlet的多个请求的时候,那这个servlet的service方法将在多线程中并发的执行。

      二、设计线程安全的Servlet

下面讨论单个Servlet、多线程情况下保证数据线程同步的几个方法。

      1、synchronized:代码块,方法。大家都会使用的方式,不用详细介绍了。 建议优先选择修饰方法。

      2、volatile 轻量级的锁,可以保证多线程情况单线程读取所修饰变量时将会强制从共享内存中读取最新值,但赋值操作并非原子性。

一个具有简单计数功能Servlet示范:

   /**
* 使用Volatile作为轻量级锁作为计数器
*
* @author yongboy
* @date 2011-3-12
* @version 1.0
*/
@WebServlet("/volatileCountDemo")
public class VolatileCountServlet extends HttpServlet {
private static final long serialVersionUID = 1L; private volatile int num = 0; protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
addOne();
response.getWriter().write("now access num : " + getNum());
} /**
* 读取开销低
*/
private int getNum() {
return num;
} /**
* 其写入为非线程安全的,赋值操作开销高
*/
private synchronized void addOne() {
num ++;
}
}

我们在为volatile修饰属性赋值时,还是加把锁的。

    3、ThreadLocal 可以保证每一个线程都可以独享一份变量副本,每个线程可以独立改变副本,不会影响到其它线程。

这里假设多线程环境一个可能落显无聊的示范,初始化一个计数,然后循环输出:

@WebServlet("/threadLocalServlet")
public class ThreadLocalServlet extends HttpServlet {
private static final long serialVersionUID = 1L; private static ThreadLocal threadLocal = new ThreadLocal() {
@Override
protected Integer initialValue() {
return 0;
}
}; protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.setHeader("Connection", "Keep-Alive"); PrintWriter out = response.getWriter();
out.println("start... " + " [" + Thread.currentThread() + "]");
out.flush(); for (int i = 0; i < 20; i++) {
out.println(threadLocal.get());
out.flush(); threadLocal.set(threadLocal.get() + 1);
} // 手动清理,当然随着当前线程结束,亦会自动清理调
threadLocal.remove();
out.println("finish... ");
out.flush();
out.close();
}
}

若创建一个对象较为昂贵,但又是非线程安全的,在某种情况下要求仅仅需要在线程中独立变化,不会影响到其它线程。选择使用ThreadLocal较好一些,嗯,还有,其内部使用到了WeakHashMap,弱引用,当前线程结束,意味着创建的对象副本也会被垃圾回收。 Hibernate使用ThreadLocal创建Session;Spring亦用于创建对象会使用到一点。

请注意这不是解决多线程共享变量的钥匙,甚至你想让某个属性或对象在所有线程中都保持原子性,显然这不是解决方案。

      4、Lock 没什么好说的,现在JDK版本支持显式的加锁,相比synchronized,添加与释放更加灵活,功能更为全面。

@WebServlet("/lockServlet")
public class LockServlet extends HttpServlet {
private static final long serialVersionUID = 1L; private static int num = 0;
private static final Lock lock = new ReentrantLock(); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try{
lock.lock();
num ++;
response.getWriter().println(num);
}finally{
lock.unlock();
}
}
}

必须手动释放锁,否则将会一直锁定。

   5、wait/notify 较老的线程线程同步方案,较之Lock,不建议再次使用。

   6、原子操作

原子包装类,包括一些基本类型(int, long, double, boolean等)的包装,对象属性的包装等。

@WebServlet("/atomicServlet")
public class AtomicServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final AtomicInteger num = new AtomicInteger(0); protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
PrintWriter out = response.getWriter();
out.println(num.incrementAndGet());
out.flush();
out.close();
}
}

包装类提供了很多的快捷方法,比如上面的incrementAndGet方法,自身增加1,然后返回结果值,并且还是线程安全的,省缺了我们很多手动、笨拙的编码实现

    7、一些建议

尽量不要在Servlet中单独启用线程;

使用尽可能使用局部变量;

尽可能避免使用锁;

属性的线程安全:ServletContext,HttpSession,ServletRequest对象中属性。

1、ServletContext:(线程不安全)

ServletContext是可以多线程同时读/写属性的,线程是不安全的。要对属性的读写进行同步处理或者进行深度Clone()。所以在Servlet上下文中尽可能少量保存会被修改(写)的数据,可以采取其他方式在多个Servlet*享,比方我们可以使用单例模式来处理共享数据。

2、HttpSession:(线程不安全)

HttpSession对象在用户会话期间存在,只能在处理属于同一个Session的请求的线程中被访问,因此Session对象的属性访问理论上是线程安全的。

当用户打开多个同属于一个进程的浏览器窗口,在这些窗口的访问属于同一个Session,会出现多次请求,需要多个工作线程来处理请求,可能造成同时多线程读写属性。这时我们需要对属性的读写进行同步处理:使用同步块Synchronized和使用读/写器来解决。

3、ServletRequest:(线程安全)

对于每一个请求,由一个工作线程来执行,都会创建有一个新的ServletRequest对象,所以ServletRequest对象只能在一个线程中被访问。ServletRequest是线程安全的。注意:ServletRequest对象在service方法的范围内是有效的,不要试图在service方法结束后仍然保存请求对象的引用。