问题来源是同事在开发的时候,前端循环了一个ajax,到后台servlet时发现有并发问题的存在,比如前端循环了5次上传用户信息和图片,用户信息相同,图片不同,需求是我们只保存一次用户信息到数据库,同时保存这5张图片。此时servlet到数据库判断该用户存不存在的时候,这5次IO是并发的,同时发现数据库中不存在用户信息,这样会导致插入了5行用户数据(此处不考虑主键控制)。
为了解决问题,写了一个列子,简单的前端代码:index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<meta content="text/html;charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<script type="text/javascript" src="./bootstrap/js/jquery-1.10.2.min.js"></script>
<link rel="stylesheet" href="./bootstrap/css/bootstrap.min.css">
<link >
</head>
<body>
<h2>Hello World!</h2>
<button class="btn btn-primary" onclick="start()">开始</button>
<script>
function start() {
for (var i = 1; i < 5; i++) {
$.ajax({
type : "post",
url : "/daiql/TestServlet",
dateType :"json",
data : {
"action" : "test_thread"
},
success : function(json) {
}
});
}
}
</script>
</body>
</html>
思路一:使用synchronized 关键字。
synchronized (this) {
//代码块
}
后台TestServlet.java代码如下:
package com.cu.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public TestServlet() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().append("Served at: ").append(request.getContextPath());
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("UTF-8");
String action = request.getParameter("action");
if ("test_thread".equals(action)) {
//下面的语句会导致所有用户一起排队
synchronized (this) {
try {
for (int i = 1; i < 10; i++) {
Thread.sleep(1000);
System.out.println("----" + i + "----");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
}
}
}
}
}
结果如图:
此时,的确实现了并发的同步机制,但问题是,现在所有的用户进来都需要排队了,A用户提交的过程中,B用户同时提交信息,会等待A用户完全结束后,再去保存B用户信息。这简直了,就像公共厕所里就一个坑,人一多,后面的就屎了。。。性能严重下降,不是我们想要的结果。
思路二:使用session控制每一个用户进来后使用的是同一个对象,其他用户进来使用新对象。
仔细查看了下servlet的机制,servlet特点如下:
(1)单实例:
从servlet生命周期中可以知道,对于具体的某个servlet来说,servlet只会初始化一次,调用一次init(),也就是说,在整个servlet容器中只会有一个servlet的实例对象,不管我们有多少请求都是针对同一个servlet实例对象。
(2)多线程:
从线程的模型可以看到,请求的处理由多个工作线程来完成的,可以同时进行处理,同时处理的数量跟线程池的大小有关系。
(3)线程不安全:
servlet是单个实例,但又是多个线程共用实例对象,意味着servlet容器在多个线程访问同一个servlet的实例对象是没有默认加锁操作的,照成线程不安全,因为我们可能出现某一个线程正在修改servlet的实例状态,但是另外一个线程又需要读取servlet的线程状态,这个时候就会出现数据不一致的情况。
(转自:https://blog.youkuaiyun.com/xiaofengwu123/article/details/50756391)
servlet是单实例还是多线程,细节就去细究了。此时思路就是把需要同步排队的操作放到一个对象(比如叫ObjectA)的方法中,在这个方法里使用synchronized 实现同时锁,剩下我们需要做的就是在针对每一个用户只实例化一个ObjectA的对象就可以。这里就用到session和map了,我们在Servlet里实例化一个map,由于Servlet是单实例,那这个map就是唯一的了,然后我们就可以用map.putIfAbsent(userid,ObjectA) 来保存这个用户对应的唯一的ObjectA的实例。(注意,此处不能用map.put(),大家可以百度下map.putIfAbsent()和map.put()的区别,此处你只要记住map.putIfAbsent()不会覆盖已有的key对应的value值就可以)。
实现代码:
前端index.jsp
<%@page import="java.util.UUID"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
String user_id = UUID.randomUUID().toString();
System.out.println("user_id="+user_id);
session.setAttribute("user_id", user_id);
%>
<html>
<head>
<meta content="text/html;charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<script type="text/javascript" src="./bootstrap/js/jquery-1.10.2.min.js"></script>
<link rel="stylesheet" href="./bootstrap/css/bootstrap.min.css">
<link >
</head>
<body>
<h2>Hello World!</h2>
<button class="btn btn-primary" onclick="start()">开始</button>
<script>
function start() {
for (var i = 1; i < 5; i++) {
$.ajax({
type : "post",
url : "/daiql/TestServlet",
dateType :"json",
data : {
"action" : "test_thread"
},
success : function(json) {
}
});
}
}
</script>
</body>
</html>
为了测试方便,前端定义了一个随机的user_id,并放到了session中,作为一个用户的唯一标示。
后端代码:
Synchronize_Test.java (需要同步的测试类,可以放和数据库交互代码)
package com.cu.test;
public class Synchronize_Test {
public void Test() {
synchronized (this) {
try {
for (int i = 1; i < 10; i++) {
Thread.sleep(1000);
System.out.println("----" + i + "----");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
TestServlet.java 代码
package com.cu.servlet;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
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 javax.servlet.http.HttpSession;
import com.cu.test.Synchronize_Test;
@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private Map<String, Synchronize_Test> map = new HashMap<>();
public TestServlet() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().append("Served at: ").append(request.getContextPath());
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession(false);
String user_id = session.getAttribute("user_id").toString();
System.out.println(user_id);
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("UTF-8");
String action = request.getParameter("action");
if ("test_thread".equals(action)) {
Synchronize_Test s_test = new Synchronize_Test();
Synchronize_Test s_test2 = s_test;
s_test = map.putIfAbsent(user_id, s_test);
if (s_test != null) {
System.out.println(s_test);
System.out.println(map.toString());
s_test.Test();
} else {
s_test2.Test();
}
}
}
}
(注意:map.putIfAbsent()方法第一次会返回null,因为用户第一次提交的时候,用户对应的对象不存在,所以定义了一个s_test2指向原来的s_test的实例地址,防止丢失)
执行结果:
至此问题解决,每个用户多并发的时候,servlet都是针对单个用户进行同步操作,不影响其他用户。