MVC多线程安全问题
成员变量数据复用问题
在实际互联网的访问环境下,表现层会接收来自多个用户的访问请求,但是mvc默认是单例模式的,进入的都是同一个单例的Controller对象,并对此成员变量的值进行修改操作,因此会互相影响,无法达到并发安全
- 我们所希望的是不同请求所使用的数据是互不影响的
多线程的并发不安全性
@Controller
@RequestMapping("thread")
public class ThreadTestController {
private Integer currentNum = 0;
@GetMapping("connection")
String getConnection(){
System.out.println("前端成功进行了一次访问,当前线程:"+Thread.currentThread().getName());
System.out.println("当前数据为:"+(++currentNum));
return "threadTest";
}
}
我们会发现多次访问此url,currentNum
是自增的
使用Scope注解
解决方案
单例模式改原多例模式
对web项目,可以Controller类上加注解**@Scope(“prototype”)或@Scope(“request”)**,对非web项目,在Component类上添加注解@Scope(“prototype”)
- 优点:实现简单
- 缺点:示例的创建销毁增加了服务器负担
使用ThreadLocal类
ThreadLocal叫做线程变量
ThreadLoacl中填充的变量属于当前线程,该变量对于其它线程而言是隔离的,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,如此就不存在多线程数据共享的问题
一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个项的key值(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的
发送30次url请求进行测试
@Controller
@RequestMapping("thread")
public class ThreadTestController {
private ThreadLocal<Integer> currentNum = new ThreadLocal<>();
@GetMapping("connection")
ModelAndView getConnection(String user, ModelAndView modelAndView){
if (currentNum.get() == null){
currentNum.set(0);
}
//对数据进行操作
currentNum.set(currentNum.get().intValue()+1);
System.out.println("前端成功进行了一次访问,当前线程:"+Thread.currentThread().getName());
System.out.println("当前数据为:"+currentNum.get().intValue()+"当前用户为:"+user);
modelAndView.addObject("user",user);
modelAndView.addObject("th",Thread.currentThread().getName());
modelAndView.setViewName("threadTest");
return modelAndView;
}
}
二十多次的连续请求得到的结果有0有1有2等等,而我们期望不管我并发请求有多少,每次的结果都是0,同时可以发现web服务器默认的请求线程池大小为10,这10个核心线程可以被之后不同的Http请求复用(数据出现了复用),所以这也是为什么相同线程名的结果不会重复的原因
ThreadLocal的方式可以达到线程隔离,但还是无法达到并发安全
推荐使用局部变量
@Controller
@RequestMapping("thread")
public class ThreadTestController {
@GetMapping("connection")
ModelAndView getConnection(String user, ModelAndView modelAndView){
int i = 0;
System.out.println("前端成功进行了一次访问,当前线程:"+Thread.currentThread().getName());
System.out.println("当前数据为:"+(++i)+"当前用户为:"+user);
modelAndView.addObject("user",user);
modelAndView.addObject("th",Thread.currentThread().getName());
modelAndView.setViewName("threadTest");
return modelAndView;
}
}
使用局部变量后就不会产生因单例模式而导致的数据复用问题,在使用前局部变量会被创建出来,在方法区方法执行完毕后局部变量会交由GC机制自动回收
使用并发安全的类
Java作为功能性超强的编程语言,API丰富,如果非要在单例bean中使用成员变量,可以考虑使用并发安全的容器,如ConcurrentHashMap、ConcurrentHashSet等,将我们的成员变量(一般可以是当前运行中的任务列表等这类变量)包装到这些并发安全的容器中进行管理即可
分布式或微服务的并发安全
如果还要进一步考虑到微服务或分布式服务的影响,方式4便不足以处理了,所以可以借助于可以共享某些信息的分布式缓存中间件如Redis等,这样即可保证同一种服务的不同服务实例都拥有同一份共享信息(如当前运行中的任务列表等这类变量)
mvc的线程安全问题
需求:请求提交用户名,后端将用户名数据封装到成员变量username中,然后返回给前端并回显
模拟用户1在请求时的网络延迟(10000ms)
对用户2请求不做网络延迟处理
此时会出现线程安全问题,由于mvc的单例模式,用户2的username信息会覆盖之前用户1的username信息
用户1请求处理完逻辑,之前存入的username值被覆盖为user2,出现线程安全问题
下面是代码模拟
private String username = "null";
@GetMapping("connection")
ModelAndView getConnection(String user, ModelAndView modelAndView){
modelAndView.setViewName("threadTest");
System.out.println("线程"+Thread.currentThread().getName()+"已进入方法体,访问用户:"+user);
//更新当前username
username = user;
//模拟网络延迟
if (user.equals("user1")){
System.out.println("----用户1出现网络延迟-----");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("前端成功进行了一次访问,当前线程:"+Thread.currentThread().getName());
//为前端返回用户数据
modelAndView.addObject("user",username);
modelAndView.addObject("th",Thread.currentThread().getName());
return modelAndView;
}
此时我们必须要使用成员变量因此无法使用局部变量的方法来解决
解决方案
使用ConcurrentHashMap
private ConcurrentHashMap<String,String> username = new ConcurrentHashMap();
@GetMapping("connection")
ModelAndView getConnection(String user, ModelAndView modelAndView){
modelAndView.setViewName("threadTest");
System.out.println("线程"+Thread.currentThread().getName()+"已进入方法体,访问用户:"+user);
//更新当前username
username.put(user,user);
//模拟网络延迟
if (user.equals("user1")){
System.out.println("----用户1出现网络延迟-----");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("前端成功进行了一次访问,当前线程:"+Thread.currentThread().getName());
//为前端返回用户数据
modelAndView.addObject("user",username.get(user));
modelAndView.addObject("th",Thread.currentThread().getName());
return modelAndView;
}
使用线程同步代码块解决并发问题
以下使用hashMap来模拟高并发,出现了线程安全问题,在结果数据中出现了0,1,2
@Controller
@RequestMapping("thread")
public class ThreadTestController {
private HashMap<String,Integer> num = new HashMap<>();
@GetMapping("connection")
ModelAndView getConnection(String user, ModelAndView modelAndView){
System.out.println("前端成功进行了一次访问,当前线程:"+Thread.currentThread().getName());
//模拟延迟
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//设置当前num记录为0
num.put("num",0);
//增加num的值
num.put("num",num.get("num")+1);
System.out.println(Thread.currentThread().getName()+"线程:当前数据为:"+num.get("num")+"当前用户为:"+user);
modelAndView.addObject("user",user);
modelAndView.addObject("th",Thread.currentThread().getName());
modelAndView.setViewName("threadTest");
return modelAndView;
}
}
使用线程同步代码块对会发生线程安全问题的代码上锁
private HashMap<String,Integer> num = new HashMap<>();
@GetMapping("connection")
ModelAndView getConnection(String user, ModelAndView modelAndView){
System.out.println("前端成功进行了一次访问,当前线程:"+Thread.currentThread().getName());
//模拟延迟
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (num){
//设置当前num记录为0
num.put("num",0);
//增加num的值
num.put("num",num.get("num")+1);
System.out.println(Thread.currentThread().getName()+"线程:当前数据为:"+num.get("num")+"当前用户为:"+user);
}
modelAndView.setViewName("threadTest");
return modelAndView;
}
总结
在mvc框架中对每个前端请求都会开放一个线程来处理本次请求,并且支持异步处理,在高并发的场景下
jmeter压力测试
jmeter官网:https://jmeter.apache.org/
运行bin目录下的jmeter.bat 打开图形界面
模拟高并发
1、右键Test plan
2、选择添加 > 线程 > 线程组
设置线程
线程数:请求并发数
Ramp-Up时间:请求间隔时间
循环次数:循环请求次数
建新http请求
1、右键线程组
2、选择添加 > 取样器 > http请求
设置请求ip,端口,路径,执行任务模拟高并发
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以邮件至 1300452403@qq.com