MVC多线程安全问题

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中使用成员变量,可以考虑使用并发安全的容器,如ConcurrentHashMapConcurrentHashSet等,将我们的成员变量(一般可以是当前运行中的任务列表等这类变量)包装到这些并发安全的容器中进行管理即可

分布式或微服务的并发安全

如果还要进一步考虑到微服务或分布式服务的影响,方式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

文章标题:MVC多线程安全问题

字数:2.1k

本文作者:Os467

发布时间:2022-08-29, 17:18:53

最后更新:2022-09-05, 00:08:53

原始链接:https://os467.github.io/2022/08/29/MVC%E8%AE%BF%E9%97%AE%E5%A4%9A%E7%BA%BF%E7%A8%8B%E9%97%AE%E9%A2%98/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

×

喜欢就点赞,疼爱就打赏