浅谈springboot如何保证多线程安全
目录
- 如何保证多线程安全
- 1.springboot在多线程并发访问下是怎么做的
- 2.controller在多线程下如何尽可能保证线程安全,如何取舍
- 3.小结一下
- 单例模式与线程安全问题踩的坑
- 下面上一张该类的截图
- 现在说下解决方法
如何保证多线程安全
1.springboot在多线程并发访问下是怎么做的
我们在Controller下,一般都是@AutoWired一些Service,由于这些Service都交给了spring进行管理,因此他们单例的,对于在Controller中调用他们的方法,由于方法在JVM中属于栈操作,所以对于每一个线程来说,栈都是独立的,因此是线程安全的。
而由于Controller本身是单例模式 (非线程安全的), 这意味着每个request过来,系统都会用原有的instance去处理,这样导致了两个结果:一是我们不用每次创建Controller,二是减少了对象创建和垃圾收集的时间;由于只有一个Controller的instance,当多个线程调用它的时候,它里面的instance变量就不是线程安全的了,会发生窜数据的问题。
如果我们定义了一个全局的实例,如 private Company company = new Company(); 而在@RequestMapping方法中去用到了他, 这里就存在并发线程安全的问题。
对于所有的请求request,这个company对象是相通的。
当然我们也可以用这个特性来制作访问计数器 只需要定义一个private int cout = 0; 在每一次请求后cout++;
当然我并不推荐这么做,计数器最好用redis来操作。
总结以上问题,不要在Controller里出现类的实例。即便加了线程安全操作,也会出现性能问题。当然无论是Controller还是Service,如果你一定要使用对象的属性,如private Company company = new Company();可以加上ThreadLocal的引用,如private ThreadLocal<Company> tc = new ThreadLocal<>();但是把这种使用的对象放进方法中初始化(即进入JVM栈中更好)。
2.controller在多线程下如何尽可能保证线程安全,如何取舍
当多个请求对controller进行请求时,它的instance的单例模式是线程不安全的,因此我们如果要保证完全的线程安全,需要对于每次请求都创建一个新的controller实例,在spring中使用@RequestScope注解定义它的作用域为requst,即一次请求即为一个实例,这样就可以保证controller层面上的线程安全。但是这样做会有一个很大的缺点,就是这种方式当并发很大时,创建bean的新实例就比重用原有的controller实例要慢许多。
因此还有折中的办法,就是将@RequestScope设置为session级别的作用域,这样每当一次会话,spring就会创建一个controller实例,而不需要每次请求都去创建一次实例,大大提高了访问的速度,虽然这样无法保证绝对的线程安全,但是在大部分的业务逻辑上都有效的防止了线程安全的问题。
此外,spring的作用域还有singleton(单例,也是spring默认的作用域级别,即永远使用同一个实例)、prototype(原型)、globalSession(全局)
3.小结一下
Spring本身并没有解决并发访问的问题。如果bean的范围不是线程安全的(例如在controller上面的成员变量或者静态变量就是线程不安全的),但其方法包含一些您总是希望安全运行的关键代码或者使用了静态字段需要对其进行并发修改,请在该方法上使用synchronized关键字。或者使用一些有提供线程安全的集合进行相应的多线程操作。
单例模式与线程安全问题踩的坑
最近有客户反映,使用公司产品时,偶尔会存在崩溃情况,自己测试无问题,然后去查日志,是报空指针。于是顺藤摸瓜 往上找,好嘛,之前的开发使用了成员变量,感觉问题就是在这里了,因为众所周知,springboot 采用的是单例模式,所以,使用成员变量时一定要谨慎。
下面上一张该类的截图
大家可能看到了,该类上面加上了@Scope("prototype") 注解,该注解的作用是将该类变成多例模式。讲道理因为变为了多例,应该不会有线程问题了。
我先说下我这边的一个代码环境,上面大家看到的BaseController这个类里面有个init方法,会在继承它的类的所有方法前执行。
使用的是@ModelAttribute注解,这个注解的意思是,在该controller的所有方法前执行,意在初始化,我猜测之前的同事应该是为了获取相同的一些参数,抽调出来做一个父类,随着迭代,别的同事为了方便,拿来就用,导致很多controller继承了该类。
@Scope("prototype")注解:大家设想一下,若父类加了@Scope("prototype")注解,子类controller并没有加该注解,会怎样呢?该注解是否还有意义?再比如,我在某service上加上@Scope("prototype")注解,但调用的controller没有加@Scope("prototype")注解,那么会出现什么样的结果呢?大家可以去测试一下,测试方法也很简单,就是在对应的父类或service的无参构造方法里打印该类的地址。
下面说下我的测试结果:先说父类上加了@Scope("prototype")注解,子类上没有加这种情况。结果是,同一子类继承的为同一父类,不同子类继承为不同父类。理解一下,很简单,因为springboot为单例模式,所以子类为单例,那么只有一个子类,父类肯定是一样的。所以,不同线程过来使用的为同一变量,就会有问题。
同理:在service上标注@Scope("prototype")注解,那在同一个controller里,该service还是同一个,也就是说还是单例的,在不同的controller里 是不同的。测试方法同上。
现在说下解决方法
1、是在继承该controller的子类上都加上@Scope("prototype")注解。这样做的好处是简单。坏处也同样明显,因为是多例的,那么就会产生大量的实体类,占用大量内存,若是回收不及时,有可能会出现内存溢出。
2、是将变量私有化,比如使用线程变量,对变量加锁等,技术上会复杂一些,而且调试不太好调试。说不定那些地方就会出现问题,毕竟是老代码。
3、将该类转换为拦截器,将变量放入request里,用的时候取出来。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。