SpringMVC:如何保证Controller的并发安全

createh52周前 (04-11)技术教程4

单例模式(Singleton)是程序设计中一种非常重要的设计模式,设计模式也是Java面试重点考察的一个方面。面试经常会问到的一个问题是:SpringMVC中的Controller是单例还是多例,很多同学可能会想当然认为Controller是多例,其实不然。

根据Tomcat官网中的介绍,对于一个浏览器请求,tomcat会指定一个处理线程,或是在线程池中选取空闲的,或者新建一个线程。

Each incoming request requires a thread for the duration of that request. If more simultaneous requests are received than can be handled by the currently available request processing threads, additional threads will be created up to the configured maximum (the value of the maxThreads attribute). If still more simultaneous requests are received, they are stacked up inside the server socket created by the Connector, up to the configured maximum (the value of the acceptCountattribute). Any further simultaneous requests will receive "connection refused" errors, until resources are available to process them.

—— https://tomcat.apache.org/tomcat-7.0-doc/config/http.html

在Tomcat容器中,每个servlet是单例的。在SpringMVC中,Controller 默认也是单例。采用单例模式的最大好处,就是可以在高并发场景下极大地节省内存资源,提高服务抗压能力。

单例模式容易出现的问题是:在Controller中定义的实例变量,在多个请求并发时会出现竞争访问,Controller中的实例变量不是线程安全的。

Controller不是线程安全的

正因为Controller默认是单例,所以不是线程安全的。如果用SpringMVC 的 Controller时,尽量不在 Controller中使用实例变量,否则会出现线程不安全性的情况,导致数据逻辑混乱。

举一个简单的例子,在一个Controller中定义一个非静态成员变量 num 。通过Controller成员方法来对 num 增加。

@Controller
public class TestController {
    private int num = 0;
    
    @RequestMapping("/addNum")
    public void addNum() {
        System.out.println(++num);
    }
}

在本地运行后:

  • 首先访问 http:// localhost:8080 / addNum,得到的答案是1;
  • 再次访问 http:// localhost:8080 / addNum,得到的答案是 2。
  • 两次访问得到的结果不同,num已经被修改,并不是我们希望的结果,接口的幂等性被破坏。

从这个例子可以看出,所有的请求访问同一个Controller实例,Controller的私有成员变量就是线程共用的。某个请求对应的线程如果修改了这个变量,那么在别的请求中也可以读到这个变量修改后的的值。

Controller并发安全的解决办法

如果要保证Controller的线程安全,有以下解决办法:

  • 尽量不要在 Controller 中定义成员变量
  • 如果必须要定义一个非静态成员变量,那么可以通过注解 @Scope(“prototype”),将Controller设置为多例模式。
@Controller
@Scope(value="prototype")
public class TestController {
    private int num = 0;
    
    @RequestMapping("/addNum")
    public void addNum() {
        System.out.println(++num);
    }
}

Scope属性是用来声明IOC容器中的对象(Bean)允许存在的限定场景,或者说是对象的存活空间。在对象进入相应的使用场景之前,IOC容器会生成并装配这些对象;当该对象不再处于这些使用场景的限定时,容器通常会销毁这些对象。

Controller也是一个Bean,默认的 Scope 属性为Singleton,也就是单例模式。如果Bean的 Scope 属性设置为 prototype 的话,容器在接受到该类型对象的请求时,每次都会重新生成一个新的对象给请求方

  • Controller 中使用 ThreadLocal 变量。每一个线程都有一个变量的副本。
public class TestController {
	   private int num = 0;
	   private final ThreadLocal  uniqueNum =
	            new ThreadLocal  () {
	                @Override protected Integer initialValue() {
	                    return num;
	                }
	            };
	
	   @RequestMapping("/addNum")
	   public void addNum() {
 		     int unum = uniqueNum.get();
		     uniqueNum.set(++unum);
		     System.out.println(uniqueNum.get());
    }
}

以上代码运行以后,每次请求 http:// localhost:8080 / addNum , 得到的结果都是1。关于ThreadLocal的原理可以参考前文《Java面试常考问题:ThreadLocal的基本原理 》。

更严格的做法是用AtomicInteger类型定义成员变量,对于成员变量的操作使用AtomicInteger的自增方法完成。

总的来说,还是尽量不要在 Controller 中定义成员变量为好。

我会持续更新关于物联网、云原生以及数字科技方面的文章,用简单的语言描述复杂的技术,也会偶尔发表一下对IT产业的看法,欢迎大家关注、转发和评论,谢谢。

相关文章

如何在Java中实现线程安全?总结如下

在Java中,线程安全是指在多线程环境下,多个线程可以安全地访问共享资源或数据,而不会出现不一致或意外的结果。以下是一些实现线程安全的常用方法:1、使用synchronized关键字: 通过在方法或代...

Java多线程与锁机制详解:打造高效安全的并发世界

Java多线程与锁机制详解:打造高效安全的并发世界在当今这个数据处理量爆炸的时代,单线程程序已经难以满足高性能需求。Java作为一门优秀的编程语言,提供了强大的多线程支持,而锁机制正是保证多线程安全的...

揭秘Java局部变量线程安全的真相:为什么它天生免疫并发问题

··在Java并发编程中,线程安全是一个永恒的话题。你是否曾疑惑:为什么局部变量不需要加锁就能避免并发问题?本文将深入剖析其底层原理,结合实战案例,带你彻底理解这一设计精髓。(点击收藏,解锁高薪面试必...

Java线程安全

当多个线程处理相同的数据,数据值发生变化时,会得到不一致的结果,这种情况不是线程安全的。 当一个线程已经在一个对象上工作并阻止另一个线程在同一个对象上工作时,这个过程称为线程安全。线程安全体现原子性:...

Java 线程安全思路

线程安全1、先来了解一下:为什么多线程并发是不安全的?****在操作系统中,线程是不拥有资源的,进程是拥有资源的。而线程是由进程创建的,一个进程可以创建多个线程,这些线程共享着进程中的资源。所以,当线...

从双重检查锁到枚举单例:Java线程安全实践中的道与术

2022年双十一前夕,笔者所在团队的全局配置管理类突然出现诡异现象:在2000+QPS的配置刷新场景下,日志中频繁出现多个ConfigManager实例的哈希码。这直接导致部分服务器读取到过期配置,险...