当你什么都不是的时候,你就无所畏惧。——《冰与火之歌》
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()方法内是有效的。