Bootstrap

(学习笔记)关于SpringMVC中的Controller、Service、DAO的多线程问题

SpringMVC中Controller为什么能够处理并发访问?

SpringMVC中用来处理http请求的Controller是基于Servlet实现的,Spring中绝大多数的类都是单例的,Servlet也是这样。

Controller、Service、DAO都是默认单例模式

既然Controller是单例模式,那么它是怎么能够在同时处理很多个请求的呢?

想要搞明白这点,首先面临的一个问题是:计算机是如何处理一个请求的呢?

计算机大部分的任务都是由CPU来完成的,Controller虽然叫做控制器,但是实际上执行处理任务的角色是CPU。控制器只是提供了CPU处理请求的方法,所以实际上是CPU根据Controller中的代码来处理。

那么是谁来控制CPU来进行任务呢?当然是进程了,在我们面对的计算机中,进程是运行的基本单元。

所以计算机是如何处理一个请求的呢?请求是由计算机中某个进程根据特定的指令来处理的。

根据这一点,我们可以知道,当服务器收到一个请求后,会有一个进程来处理它,把这个请求经过拦截器等等不同的处理程序,终于来到了控制器了,控制器对它进行了一些处理,然后又把它交给下一步的程序处理(实际上的实施主体是进程),经过一些处理,这时就可以叫处理过后的数据为响应了,进程把这些数据发送到某个接受的地方,一次Http请求就完成了。

在这个过程中,真正操作的是一个进程,代码是存放在内存中的一段一段数据,进程从中读取数据,也许会对其中的某些数据进行修改(这里就涉及到了多线程的安全问题)。

而这一次处理请求并返回响应的过程,在实际中操作的是一个线程,它在主进程中创建,用于处理一个请求。

当多个请求同时访问服务器的时候

现在,有多个请求同时访问服务器,每个请求都有一个线程来处理,线程由服务器程序来创建(例如SpringBoot默认使用的Tomcat),线程根据内存中的代码(代码相当于说明书)执行下去,每个线程都可以访问到Controller中的代码,如果Controller只有一个的话,那每个线程都访问这个Controller,根据它的代码来执行。代码就像是一份说明书,无论多少的请求,都按照同一份说明书来处理。

知道了每个请求都是由一个线程来处理,我们也就可以明白一个服务器同时能够处理的请求数与它的线程数有很大的关系。线程的创建是比较消耗资源的,所以容器一般维持一个线程池。像Tomcat的线程池 maxThreads 是200, minSpareThreads 是25。实际中单个Tomcat服务器的最大并发数只有几百,部分原因就是只能同时处理这么多线程上的任务。当然,并发的限制肯定不止在这里,还有很多需要考虑的地方。

因此,应对请求分配线程处理的是servlet容器(也就是tomcat等服务器程序)。

Controller、Service、DAO是线程安全的吗?

关于类中的变量

首先要先说一下几个基本概念:
1、静态变量:线程非安全。
静态变量即类变量,位于方法区,为所有对象共享,共享一份内存,一旦静态变量被修改,其他对象均对修改可见,故线程非安全。

2、实例变量:单例模式(只有一个对象实例存在)线程非安全,非单例线程安全。
实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变量的修改将互不影响,故线程安全。

3、局部变量:线程安全。
每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题

Controller、Service、DAO等类都默认为单例模式

我们知道, Controller、Service、DAO都是默认为单例模式的,

又因为,如果一个类无论什么时候都不会改变,那么它就是线程安全的,无论多少线程同时访问,都会得到相同的结果,不会有任何影响,不用考虑多线程带来的影响。

所以在Controller、Service、DAO尽量使用局部变量,不要使用类的成员变量,如果使用的话,记得一定要加锁。

控制器中如果没有维持可变的成员变量,也类似于不可变类,它在多线程情况下也不需要多考虑,和在单线程下区别不大,当然这一般不会发生。我们经常在其中定义许多Service,在容器启动的时候这些Service被注入进来,用户传入的请求大部分在这里和服务器进行交互,比如查看当前是否登录,请求查看用户信息等等,根据Controller中的代码,调用不同的Service对这些信息进行处理。这里就要考虑到线程安全的问题了。

Controller、Service、DAO等类中的方法当中的并发问题

尽管 Controller、Service、DAO都是默认为单例模式的,

但是每个方法在调用栈里都会有自己独立的栈帧,每个栈帧里都有对应方法需要的参数和返回地址。当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出。

栈帧是在调用方法时创建,方法返回时“消亡”。

局部变量存放在哪里?
局部变量的作用域在方法内部,当方法执行完,局部变量也就没用了。可以这么说,方法返回时,局部变量也就“消亡”了。此时,我们会联想到调用栈的栈帧。没错,局部变量就是存放在调用栈里的。此时,我们可以将方法的调用栈用下图表示。

线程封闭
方法里的局部变量,因为不会和其他线程共享,所以不会存在并发问题。这种解决问题的技术也叫做线程封闭。仅在单线程内访问数据。由于不存在共享,所以即使不设置同步,也不会出现并发问题。

所以在Controller、Service、DAO尽量使用局部变量,不要使用类的成员变量,如果使用的话,记得一定要根据业务逻辑来判断是否要加锁。

关于DAO并发访问数据的问题

假设一个例子,现在要做一个用户注册服务,用户注册需要绑定手机号码,因此,注册的业务逻辑中必须要有一个判断,也就是判断该手机号码有没有被注册,这需要DAO层去数据库查询是否有拥有该手机号码的记录。

现在有两个注册请求同时发出,带着一样的电话号码。这两个请求同时到达Tomcat服务器,在两个线程内同时调用Controller,在DAO层查询电话号码的结果都是“该手机号码没有被注册”,于是都用该电话号码进行了注册,但是由于数据库库中,用户表中,电话号码这个属性被设置了unique key,所以这两个注册请求一定会有一个请求发生异常,因此,做这种功能的时候一定要加上异常处理。

;