关于Servlet中针对单一用户并发问题解决

问题来源是同事在开发的时候,前端循环了一个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.csdn.net/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都是针对单个用户进行同步操作,不影响其他用户。

猜你喜欢

转载自blog.csdn.net/qingliang4321/article/details/81107292