这是我参与更文挑战的第1天,活动详情查看:更文挑战
问题由来
在浏览资料的时候看到Spring默认是采用单例模式来管理Bean对象,那么自己就想到,这里面是否会有线程安全问题?
资料汇总和自己的理解
单例和多例
- Spring容器中对象默认是以单实例方式实现
- 可以显式使用@Scope(“prototype”)注解来实现多实例
- singleton单实例在IOC启动时调用方法创建对象并放入容器中,以后每次调用都是从容器Map里面获取同一个bean
- prototype多实例在IOC启动时不会调用方法创建对象,而是在每次调用的时候创建对象
例:
多实例模式
@Scope("prototype")
public class MultViewController {
private static int st = 0;
private int index = 0;
@RequestMapping("/test")
public String test() {
System.out.println(st++ + " | " + index++);
return "/lsh/ch5/test";
}
}
复制代码
各自的使用场景
用单例是因为没必要每个请求都新建一个对象,这样子既浪费CPU又浪费内存
用多例是为了防止并发问题;即一个请求改变了对象的状态,此时对象又处理另一个请求,而之前请求对对象状态的改变导致了对象对另一个请求做了错误的处理
当对象含有可改变的状态时用多例,否则用单例
单例模式的线程安全
Spring中的Controller默认是单例模式,因此在Controller中不能使用非静态的成员变量,否则会发生数据逻辑混乱,即线程不安全
几个解决方案:
- 不要在controller中定义成员变量
- 万一必须要定义一个非静态成员变量时候,则通过注解@Scope(“prototype”),将其设置为多例模式
- 在Controller中使用ThreadLocal变量
单例的底层原理
下面是结合自己做后端和iOS开发整理的一些思路:
- 首先说方法或函数,C语言中的函数代码会放在内存结构中会放在代码区,多线程执行的时候,每个线程在自己的frame内创建局部变量,然后根据代码区的入口逐行执行代码; 而在面向对象语言中,比如C++或Java,在底层内存结构上其实并没有改变多少,核心原理是相同的,它会将Java类中的方法提取出来,仍然放到代码区,只不过两点不同:在编译过程中起的内部名字中含有类名信息,第二个是在调用类方法时,默认会把类对象作为一个参数传入进来,整体上和C语言的底层机制是一样的
- 单例模式就是整个应用中只创建一个类对象,如果类方法只是对局部变量操作,多个线程彼此之间互不影响,那么这种是线程安全的,也是最适合单例模式的场景,因为从底层代码原来来看,仍然是每个线程内部生成局部变量,互不影响,然后各自逐步执行方法入口处的汇编代码,比如方法实现就是从mysql获取结果,那么这个就是线程安全的,可以以单例模式执行,彼此查询互不影响; 另外一个例子,比如ios中的网络组件那里,因为每一个都要记录请求状态数据,因此不能用单例模式,而是每一个Controller中都创建一个AFKManager的实例,用来发送请求
局部变量不受多线程影响
对于那些会以多线程运行的单例类,例如Web应用中的Servlet,每个方法中对局部变量的操作都是在线程自己独立的内存区域内完成的,所以是线程安全的
成员变量受多线程影响
类的成员变量是受多线程影响的,因为是通过new来创建的类对象,所以类成员变量会放在栈中; 而一个类对象中的成员变量会按照特定的顺序在内存中存储,所以在类方法中,因为将类对象地址作为第一个参数传了进去,所以可以访问在栈中的成员变量的值; 因此多线程访问是不安全的
JVM是如何实现线程的独立内存空间
- 每当启用一个线程时,JVM就为他分配一个Java栈,栈是以帧为单位保存当前线程的运行状态。某个线程正在执行的方法称为当前方法,当前方法使用的栈帧称为当前帧,当前方法所属的类称为当前类
- 每当线程调用一个Java方法时,JVM就会在该线程对应的栈中压入一个帧,这个帧自然就成了当前帧。当执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等等。
- Java栈上的所有数据都是私有的。任何线程都不能访问另一个线程的栈数据。所以我们不用考虑多线程情况下栈数据访问同步的情况。
ThreadLocal
Java支持通过ThreadLocal来对单例模式做到线程安全:
Java里有个API叫做ThreadLocal,spring单例模式下用它来切换不同线程之间的参数。用ThreadLocal是为了保证线程安全,实际上ThreadLoacal的key就是当前线程的Thread实例。单例模式下,spring把每个线程可能存在线程安全问题的参数值放进了ThreadLocal。这样虽然是一个实例在操作,但是不同线程下的数据互相之间都是隔离的,因为运行时创建和销毁的bean大大减少了,所以大多数场景下这种方式对内存资源的消耗较少,而且并发越高优势越明显。
单例模式的优势
单例模式大大节省了实例的创建和销毁,有利于提高性能,而ThreadLocal用来保证线程安全性
单例模式是spring推荐的配置,它在高并发下能极大的节省资源,提高服务抗压能力。spring IOC的bean管理器是“绝对的线程安全”。