SpringMVC中Controller为什么能够处理并发访问?&Springboot中的定时任务是否会发生阻塞?

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

两个线程同时请求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,所以这两个注册请求一定会有一个请求发生异常,因此,做这种功能的时候一定要加上异常处理。




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

Tomcat官网截图如下:
在这里插入图片描述

根据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 <Integer> uniqueNum =
	            new ThreadLocal <Integer> () {
	                @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。

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

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




关于controller的一点疑问

一般情况下,spring中的controller是单例模式的,也就是说所有的访问都会调用同一个controller的方法,自然而然的就会想到并发的问题,当某一个请求调用controller方法尚未退出时,是否会造成后续请求的阻塞。
写个小demo测试一下

	@RequestMapping("/dotest01/{id}")
	@ResponseBody
	public String dotest01(@PathVariable("id") int id) {
		long start =System.currentTimeMillis();
		String str;
		if(id==1) {
			try {
				Thread.sleep(4000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			log.info("1号"+Thread.currentThread().getName());
			log.info(this.toString());
			str="1号结果!";
		}else {
			log.info("2号"+Thread.currentThread().getName());
			log.info(this.toString());
			str="2号结果";
		}
		long time =System.currentTimeMillis()-start;
		return str+time;
	}

进行测试先发送访问dotest01/1,后访问dotest01/2,很明显第一个页面尚在加载,第二个就已经返回
在这里插入图片描述在这里插入图片描述
再看看后头的对象
在这里插入图片描述
很明显,线程是单独的但是controller是同一个。
可以确认的是,不用考虑controller的阻塞问题,再写一个多线程测试案例

public class TestThread {
	public static void main(String[] args) {
		MyTool tool = new MyTool();
		new Thread(new MyThread(0, tool)).start();
		new Thread(new MyThread(1, tool)).start();
	}
}

class MyThread implements Runnable {
	
	private int id;
	
	private MyTool tool;
	
	public MyThread(int id,MyTool tool) {
		this.id=id;
		this.tool=tool;
	}
	
	public void run() {
		long start = System.currentTimeMillis();
		System.out.println("Thred:"+Thread.currentThread().getName()+"=="+tool.toString());
		tool.dosome(id);
		long time = System.currentTimeMillis()-start;
		System.out.println("id"+id+"==time="+time);
	}
}

 class MyTool {
	public void dosome(int id) {
		if(id ==0) {
		System.out.println("00开始工作!");
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("00工作完成!");
		}else {
			System.out.println("01开始工作!");
			System.out.println("01工作完成!");
		}		
	}
}

测试结果为在这里插入图片描述
符合期望,工作线程不同,工作对象同一个。在修改一下

public synchronized void dosome(int id){
		.....
}

为方法加上锁,这时候就不同了,在这里插入图片描述
此时线程才会阻塞,说到底对阻塞的概念有点混乱,多线程是可以同时访问通一个不加锁方法,所谓额并发问题是多线程操作造成的数据混乱,阻塞是加锁造成的,很显然controller并未给方法加锁,所以并不会有阻塞的问题。
但是也得注意,在controller中创建全局变量这时候就要考虑并发问题了。




Springboot中的定时任务是否会发生阻塞?

Springboot中一个定时任务没执行完,是否会影响下一个定时任务执行?

在springboot中使用定时任务的步骤

1.在启动类上加上注解:@EnableScheduling,表示允许定时任务执行

2.定时任务需要在类上加上@Component或者其衍生类(Controller、Service等),用于纳入Spring容器管理。

3.在需要定时任务方法上增加注解@Scheduled,注解的参数是定时任务执行时机

首先需要知道:定时任务默认是单线程的。所以默认情况下,上一个定时任务没有执行完,下一个定时任务是不会开始的

结论:
1.定时任务默认是单线程的。如果任务执行时间超过定时任务间隔时间,不管是同一个定时任务还是不同的定时任务,下一个任务都会被阻塞。
2.实现SchedulingConfigurer接口后,定时任务会变成多线程执行。不同的定时任务之间互不影响,同一个定时任务(方法)依然会有被阻塞的机制。
3.如果定时任务交给线程池处理,则下一个任务不会被阻塞。

  • 14
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Archie_java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值