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