导读
服务器虽然能够为每一个会话维护一个Session,在一定程度上确保了线程安全,但是往往在一些细节上还是不能如愿地保证线程安全。这里将详细介绍线程不安全的情况。
HttpServletRequest
源码
通过CTRL
+鼠标左键
,你找到了源码。(Tomcat相关的jar包有三个,直接找需要很久)
先来看注释:
Extends the javax.servlet.ServletRequest interface to provide request information for HTTP servlets. The servlet container creates an HttpServletRequest object and passes it as an argument to the servlet's service methods(doGet, doPost, etc).
继承了ServletRequest
类,为HTTP Servlet
一共请求头信息。这个Servlet容器创建了一个HttpServletRequest
对象并作为参数传递给Servlet的服务方法,比如doGet、doPost等。
非常清晰了吧?就算HttpServletRequest
是一个接口,他在运行的时候还是创建了一个提供请求头信息的容器。而其中的getSession
方法一共有两个,一个是带有create
参数的方法,一个是无参方法。
首先是有参方法
public HttpSession getSession(boolean create)
他的注释是:传入true,就会在必要的时候为请求新建一个Session,返回值将会是与请求头相关联的HttpSession对象;而如果是false则会在当前没有Session的时候直接返回null,最后也会因为没有对应的有效对象直接返回null。
而无参方法则是getSession(true)
的调用。
HttpSession的源码
你似乎还是不明白这到底是怎么回事。于是,为了弄清楚getSession到底返回了个什么,你又找到了HttpSession的源码,然后发现他依然是一个接口。
老规矩,先来看看注释:
Provides a way to identify a user across more than one page request or visit to a Web site and to store information about that user.
提供了在多个页面请求中确认其中一个用户身份的方法,或者说是访问Web服务器并存储用户信息的方法。
说白了这就是实现了Session的功能。下面更是有一句你非常熟悉的:The session persists for a specified time period, across more than one connection or page request from the user.
。相信不翻译大家也能明白HttpSession是个啥玩意了吧。
但是接口怎么实现?继续顺藤摸瓜,你找到了StandardSession
,这部分参考了CSDN中digdeep的博客。
而在StandardSession
中,第一行就使用了ConcurrentHashMap
作为容器存储信息。这是个什么?因为这又是一个大坑所以还是另外用一个单独的版块来进行阐述,点击这里查看ConcurrentHashMap详解。这里就直接说结论了:ConcurrentHashMap
为HttpSession
保证了线程安全,这个是不容置疑的。
Session线程不安全的真正原因
事实上,Servlet
本身不是线程安全的,就算你的Session
再怎么线程安全,最后Servlet
中修改变量值的时候永远都会出现问题。举个例子:
// 在Servlet中定义属性
private int count;
// 在Servlet中编写方法
public void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException, InterruptedException {
// 属性修改
count++;
// 线程暂停,放大效果
Thread.sleep(1000);
// 输出结果
System.out.println(Thread.currentThread().getName() + "count: " + count);
}
/**
* 使用tomcat自带的JMETER压力测试工具模拟五次访问,输出结果就是这样:
* http-bio-8080-exec-6 count: 5
* http-bio-8080-exec-9 count: 5
* http-bio-8080-exec-8 count: 5
* http-bio-8080-exec-5 count: 5
* http-bio-8080-exec-7 count: 5
*/
很明显,五次输出中四次都拿到了脏数据,这不是能够让系统中断的错误,这是逻辑错误,往往比编译器报错更让人头疼。会出现这种情况的原因有两点:
- 容器只会创建一个
Servlet
实例,也就是说,这个Servlet
中定义的所有变量,只会出现一次。 - 容器收到请求之后,会启动一个线程来处理该请求。当有多个请求同时访问同一个
Servlet
值后,这只会出现一次的变量在被多个线程修改的时候就会出现线程安全问题。
所以这个问题实际上有点迷惑,真正线程不安全的并不是Session
,而是Servlet
。
那么怎么解决呢?加上关键字synchronized
。
// 在Servlet中定义属性
private int count;
// 在Servlet中编写方法
public void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 加锁,this是指当前类
synchronized (this) {
count++;
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "count: " + count);
}
}
/**
* 使用tomcat自带的JMETER压力测试工具模拟五次访问,输出结果就是这样:
* http-bio-8080-exec-5 count: 1
* http-bio-8080-exec-9 count: 2
* http-bio-8080-exec-7 count: 3
* http-bio-8080-exec-8 count: 4
* http-bio-8080-exec-6 count: 5
*/
现在就正常了。
虽然加锁能够避免线程安全问题,但是加锁会牺牲性能。
所以尽量我们不要修改Servlet
的属性。如果你使用了框架,相对应的就是Controller
部分,尽量不要修改这些部分。