Servlet线程安全问题

当你什么都不是的时候,你就无所畏惧。——《冰与火之歌》

1、引言

最近在开发中,经常理所当然的用SpringMVC处理请求,想着SpringMVC在处理高并发请求的时候怎么去解决线程安全问题。因此先研究了SPringMVC处理请求的基础,Servlet的线程安全问题,后面再去深入SpringMVC的线程安全处理机制。

2、Servlet生命周期及其处理流程

Servlet生命周期是指从创建直到毁灭的整个过程。以下是Servlet的处理过程:

  • Servlet通过调用init()方法进行初始化。
  • Servlet调用service()方法来处理客户端的请求。
  • Servlet通过调用destroy()方法终止(结束)。
  • 最后,Servlet是由JVM的垃圾回收器进行垃圾回收的。
    在这里插入图片描述

这边详细介绍下Servlet生命周期的流程:

(1)init()

init方法被设计成只调用一次。它在第一次创建Servlet时被调用,在后续每次用户请求时不再调用。因此,它是用于一次性初始化,类似单例模式。

Servlet创建于用户第一次调用对应于该Servlet 的URL时,但是您也可以指定Servlet在服务器第一次启动时被加载。

当用户调用一个Servlet时,就会创建一个Servlet实例,每一个用户请求都会产生一个新的线程,适当的时候移交给doGet或doPost方法。init()方法简单地创建或加载一些数据,这些数据将被用于Servlet的整个生命周期。

init方法的定义如下:

public void init() throws ServletException {
  // 初始化代码...
}

(2)service()

service()方法是执行实际任务的主要方法。Servlet容器(即Web服务器)调用service()方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。

每次服务器接收到一个Servlet请求时,服务器会产生一个新的线程并调用服务。service()方法检查HTTP请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGet、doPost、doPut,doDelete等方法。

service()方法的定义如下:

public void service(ServletRequest request, 
    ServletResponse response) throws ServletException, IOException{
}

service() 方法由容器调用,service 方法在适当的时候调用 doGet、doPost、doPut、doDelete 等方法。所以,一般不用对 service() 方法做任何动作,只需要根据来自客户端的请求类型来重写 doGet() 或 doPost() 即可。

doGet() 和 doPost() 方法是每次服务请求中最常用的方法。下面是这两种方法的定义。

  • doGet()
    GET请求来自于一个URL的正常请求,或者来自于一个未指定METHOD的HTML表单,它由doGet()方法处理。
public void doGet(HttpServletRequest request,
                  HttpServletResponse response)
    throws ServletException, IOException {
    // Servlet 代码
}
  • doPost()
    POST请求来自于一个特别指定了METHOD为POST的HTML表单,它由doPost()方法处理。
public void doPost(HttpServletRequest request,
                   HttpServletResponse response)
    throws ServletException, IOException {
    // Servlet 代码
}

(3)destroy()

destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。

在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收。destroy 方法定义如下所示:

public void destroy() {
    // 终止化代码...
  }

3、Servlet的多线程安全

从上面Servlet的生命周期可以看出,当第一次调用Servlet的时候,tomcat会根据web.xml配置文件实例化servlet,当后面又有请求访问该servlet的时候,不会再实例化该servlet。

Servlet容器默认是采用单实例多线程方式处理多个请求的,多线程请求方式执行的设计可大大降低对系统的资源需求,提高系统的并发量及响应时间。

线程安全是指多个线程在访问一个类时,如果不需要额外的同步,这个类的行为仍然是正确的。《Java并发实战》

Servlet本身是无状态的,一个无状态的Servlet是绝对线程安全的,无状态对象设计也是解决线程安全问题的一种有效手段。所以,servlet是否线程安全是由它的实现类来决定的。

下面是《Java并发编程实战》中的一个示例:

public class UnsafeCountingFactorizer implements Servlet{
    private long count=0;
    public long getCount(){
        return count;
    }
    @Override
    public void service(ServletRequest req, ServletResponse resp)
            throws ServletException, IOException {
        BigInteger i=extractFromRequest();
        BigInteger[] factors=factor(i);
        ++count;
    }
}

递增操作count++并非是原子操作,它包含了三个独立的操作:读取-修改-写入,其结果依赖于之前的状态。

通过上面的示例可以看出,servlet本身是无状态的线程安全的,它是否线程安全是根据它的实现来决定的,如果实现类的属性或方法会被多个线程改变,它就是线程不安全的,反之,就是线程安全的。

总结一下:多线程下每个线程对局部变量都会有自己的一份copy,这样对局部变量的修改只会影响到自己的copy而不会对别的线程产生影响,线程安全的。但是对于实例变量来说,由于servlet在Tomcat中是以单例模式存在的,所有的线程共享实例变量。多个线程对共享资源的访问就造成了线程不安全问题。

4、如何保证Servlet的线程安全

  • (1)避免使用实例变量
    这里的变量变量指的是字段和共享数据,尽量将变量局部化处理。
  • (2)避免使用非线程安全的集合
  • (3)在多个Servlet中对某个外部对象(例如文件)的修改是务必加锁,互斥访问。
  • (4)属性的线程安全
    ServletContext:它是线程不安全的,多线程下可以同时进行读写,因此我们要对其读写操作进行同步或者深度的clone。
    HttpSession:同样是线程不安全的,和ServletContext的操作一样。这里稍微看下源码:
public class StandardSession implements HttpSession, Session, Serializable {
  
    protected Map<String, Object> attributes = new ConcurrentHashMap<>();
    @Override
    public Object  getAttribute(String name) {

        if (!isValidInternal())
            throw new IllegalStateException
                (sm.getString("standardSession.getAttribute.ise"));

        if (name == null) return null;

        return (attributes.get(name));

    }

    public void setAttribute(String name, Object value, boolean notify) {

        // Name cannot be null
        if (name == null)
            throw new IllegalArgumentException
                (sm.getString("standardSession.setAttribute.namenull"));

        // Null value is the same as removeAttribute()
        if (value == null) {
            removeAttribute(name);
            return;
        }
        // ... ...
        // Replace or add this attribute
        Object unbound = attributes.put(name, value);
       // ... ...
   }
  
    @Override
    public void recycle() {
        // Reset the instance variables associated with this Session
        attributes.clear();
        // ... ...
    }
   
    protected void doWriteObject(ObjectOutputStream stream) throws IOException {
        // ... ...
        // Accumulate the names of serializable and non-serializable attributes
        String keys[] = keys();
        ArrayList<String> saveNames = new ArrayList<>();
        ArrayList<Object> saveValues = new ArrayList<>();
        for (int i = 0; i < keys.length; i++) {
            Object value = attributes.get(keys[i]);
            if (value == null)
                continue;
            else if ( (value instanceof Serializable)
                    && (!exclude(keys[i]) )) {
                saveNames.add(keys[i]);
                saveValues.add(value);
            } else {
                removeAttributeInternal(keys[i], true);
            }
        }

        // Serialize the attribute count and the Serializable attributes
        int n = saveNames.size();
        stream.writeObject(Integer.valueOf(n));
        for (int i = 0; i < n; i++) {
            stream.writeObject(saveNames.get(i));
            try {
                stream.writeObject(saveValues.get(i));  
                // ... ...            
            } catch (NotSerializableException e) {
                // ... ...               
            }
        }
    }
}

可以看到,
1)每一个独立的HttpSession中保存的所有属性,是存储在一个独立的ConcurrentHashMap中的,所以,HttpSession.getAttribute(), HttpSession.setAttribute() 等等方法都是线程安全的;
2)要保存在HttpSession中对象应该是序列化的;

虽然getAttribute,setAttribute是线程安全的了,但是我们的业务方法:

session.setAttribute("user", user);
User user = (User)session.getAttribute("user", user);

不是线程安全的!因为User对象不是线程安全的,假如有一个线程执行下面的操作:

User user = (User)session.getAttribute("user", user);
user.setName("xxx");

这里就会存在并发问题。因为会出现:有多个线程访问同一个对象 user, 并且至少有一个线程在修改该对象。但是在通常情况下,就是这么写。原因是:在web中 ”多个线程访问同一个对象 user, 并且至少有一个线程在修改该对象“ 这样的情况极少出现;因为我们使用HttpSession的目的是在内存中暂时保存信息,便于快速访问,所以我们一般不会进行上面的操作,但是的确是不安全的。

ServletRequest:它是线程安全的,对于每一个请求由一个工作线程来执行,都会创建一个。ServletRequest对象,所以ServletResquest只能在一个线程中被访问,而且只在service()方法内是有效的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值