在实际互联网的访问环境下,表现层会接收来自多个用户的访问请求,但是mvc默认是单例模式的,进入的都是同一个单例的Controller对象,并对此成员变量的值进行修改操作,因此会互相影响,无法达到并发安全
多线程的并发不安全性
1812("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请求进行测试
3312("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}
2112("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 //更新当前username10 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 //更新当前username10 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
3412("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记录为020 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记录为018 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,端口,路径,执行任务模拟高并发