初步理解并避免并发在Web系统中的问题
对于我们每个Web系统来说,我们的系统会被来自五湖四海的用户并发的访问(同时访问),并且很有可能这些用户并发访问的是Web应用中的同一个Servlet。
Servlet容器就会为每个请求分配一个工作线程,这些工作线程并发的来执行同一个Servlet对象的service()方法。
第一种并发问题:变量作用域类型导致的并发问题
package test1;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "HelloServlet1")
public class HelloServlet1 extends HttpServlet {
private String username = null;
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request,response);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
username = request.getParameter("username");//把username请求参数赋值给username变量
try {
Thread.sleep(2000);
} catch (Exception ex) {
ex.printStackTrace();
}
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("username"+": "+username);
}
}
xml映射配置为/helloservlet1
当我们配置好环境之后,打开两个浏览器同时输入两个URL:
+ http://localhost:8080/helloservlet1?username=Lucy
同时不一定要绝对的同时,时间相近就可以了(一个一个进网址,然后刷新也是可以的)
结果如下:
我们可以发现:请求参数明明是Mark,为什么返回的却是Lucy呢?
我们应该关注HTTP请求和线程,以及HTTP请求和username变量之间的对应关系。
+ 一个HTTP请求对应着一个工作线程
+ 一个HTTP请求对应着一个username变量
结论:一个工作线程一个要对应一个username变量。所以应该吧username变量定义为Service方法内部的局部变量:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = null;
username = request.getParameter("username");
try {
Thread.sleep(2000);
} catch (Exception ex) {
ex.printStackTrace();
}
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("username"+": "+username);
}
我们再来详细分析下刚刚的例子中时序操作过程:
时刻点 | 响应第一个工作请求的工作线程 | 响应第二个工作请求的线程 |
---|---|---|
T1 | 请求读取username = Mark | |
T2 | 请求读取username = Lucy | |
T3 | 将实例变量username赋值为Mark | |
T4 | 将实例变量username赋值为Lucy | |
T5 | Sleep(3000) | |
T6 | Sleep(3000) | |
T7 | username = Lucy | |
T8 | username = Lucy |
这里的时间很短,类似于CPU中的时间片我们近似认为操作是并发的
第二种并发问题:多线程同步带来的并发问题
package test2;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "HelloServlet2")
public class HelloServlet2 extends HttpServlet {
private int number=100;
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request,response);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int increase= 1;
increase = Integer.parseInt(request.getParameter("increase"));
response.getWriter().write(number + " + " + increase + " = ");
try {
Thread.sleep(2000);
} catch (Exception ex) {
ex.printStackTrace();
}
number += increase;
response.getWriter().write(number + "");
}
}
当我们配置好环境后同时打开浏览器输入两个URL:
+ http://localhost:8080/helloservlet2?increase=100
结果如下:我们会发现100+100=400
发生错误的原因:number的初始值为100,当它先接受到第二个请求increase=200之后它将会变为300,那么对于第一个请求的number来说则变成了number(300)+100=number(400)。
+ 一个HTTP请求对应的是一个工作线程。
+ 但是所有的HTTP请求对应的是同一个number变量。
解决方法:利用Java的同步机制
package test2;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "HelloServlet2")
public class HelloServlet2 extends HttpServlet {
private int number=100;
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request,response);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int increase= 1;
increase = Integer.parseInt(request.getParameter("increase"));
synchronized (this) {//同步代码版块
response.getWriter().write(number + " + " + increase + " = ");
try {
Thread.sleep(2000);
} catch (Exception ex) {
ex.printStackTrace();
}
number += increase;
response.getWriter().write(number + "");
}
}
}
这样就可以确保在任意的一个时刻,只允许一个工作线程执行servlet的service()方法,从而确保两个浏览器都可以得到当前匹配的响应结果。
SingleThreadModel接口(不推荐使用)
这是一个废弃的接口,该接口是专门用来避免并发问题而提供的。可以解决上述的两种并发问题,但均没有符合本意,仅仅是解决而已。
工作原理:
+ 在任意时刻,只允许有一个工作线程执行service()方法,如果有多可客户访问该servlet,那么这些客户的请求即将被放到一个等待队列中,容器会一次来响应等待队列中的每个客户的请求。
这种方法实际上禁止了多个客户端对同一个servlet的并发访问。
- 第一个例子如果使用SingleThreadModel接口,则还是没有改变全局变量为局部变量,这与实际应用需求不合。
- 第二个例子如果使用SingleThreadModel接口,对于两次同时访问servlet,尽管对于单次的客户请求可以返回正确的结果,但是无法用于累计客户多次请求的加法操作,使得实例失去意义。
所以在实际开发中使用SingelThreadModel不能很好的解决问题。