内容梗概:
Spring中通过配置或者注解的方式就可以动态的生成Java对象,这种方式算比较好理解的,在java.lang.reflect包中提供的反射类即可实现。但是在默认不修改Scope作用域情况下,Spring动态生成的对象(Bean实例)都是单例形式的,那么这就存在一个比较明显的问题:
在多个请求(线程)同时访问并且修改这个单例的实体对象时,Spring如何保证该对象的数据安全性呢?
首先模拟一下在并发下访问单例对象的场景,类列表如下:
类名 | 描述 | 重要方法 |
---|---|---|
Student | 实体类 | setStudentInfo() 设置对象信息 |
StudentFactory | 实体类单例工厂 | getStudent() 获取单例Student对象 |
StudentThreadLocalFactoty | 改写的实体类单例工厂 | getStudent() 获取单例Student对象 |
TraThread01 | 线程类 | run() 访问单例Student对象并操作数据 |
TraThread02 | 线程类 | run() 访问单例Student对象并操作数据 |
TestThread | 测试类 | main() 进行测试 |
实体类
public class Student {
private int id;
private String name;
private String remark;
/**setter and getter **/
//设置对象信息
public void setStudentInfo(int id,String name,String remark){
this.setId(id);
this.setName(name);
this.setRemark(remark);
}
}
饱汉式单例工厂
public class StudentFactory {
private static Student student = null;
private StudentFactory() {
}
//获取单例对象
public static Student getStudent(){
if(student == null){
synchronized(StudentFactory.class){
Thread thread = Thread.currentThread();
if(student == null){
System.out.println(thread.getName()+"----创建了pojo对象");
student = new Student();
}else{
System.out.println(thread.getName()+"----沿用了当前pojo对象");
}
}
}
return student;
}
}
线程类01和02
public class TraThread01 implements Runnable{
@Override
public void run() {
Student stu = StudentFactory.getStudent();
for(int i = stu.getId();i<100;i++){
System.out.println("线程一访问到的对象信息---"+stu.getName()
+":"+stu.getRemark()+"------"+stu.toString());
stu.setStudentInfo(i,"张三"+i, "中文备注"+i);
//.....进行其他的操作.....
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class TraThread02 implements Runnable{
@Override
public void run() {
Student stu = StudentFactory.getStudent();
for(int i = stu.getId();i<100;i++){
System.out.println("Thread 2 get the object info---"+stu.getName()
+":"+stu.getRemark()+"------"+stu.toString());
stu.setStudentInfo(i,"abc"+i, "English"+i);
//.....进行其他的操作.....
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试类
public class TestThread {
public static void main(String[] args) {
Student stu = StudentFactory.getStudent();
stu.setId(0);
TraThread01 t01 = new TraThread01();
TraThread02 t02 = new TraThread02();
new Thread(t01,"线程一").start();
new Thread(t02,"线程二").start();
}
}
根据上面的代码,测试结果为:
main—-创建了pojo对象
线程一访问到的对象信息—null:null——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—null:null——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—abc0:English0——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—abc0:English0——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—张三1:中文备注1——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三1:中文备注1——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—张三2:中文备注2——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三2:中文备注2——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—张三3:中文备注3——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三3:中文备注3——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三4:中文备注4——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—张三4:中文备注4——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—abc5:English5——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—abc5:English5——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—abc6:English6——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—abc6:English6——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—abc7:English7——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—abc7:English7——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—abc8:English8——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—abc8:English8——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—张三9:中文备注9——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三9:中文备注9——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—张三10:中文备注10——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三10:中文备注10——com.study.pojo.Student@72f4d50a
根据上面的测试结果,不难看出,对单例模式的实体操作中,由于两个线程抢占资源,线程01获取或进行操作的过程中,实体的数据极大可能被并发线程02进行修改,很显然线程01进行的数据操作已经不是准确的了。
Spring在通过IOC容器生成的单例Bean同时,会通过IOC容器为该Bean创建TreadLocal副本,以保证多线程并发时对该单例Bean数据访问时的安全性。
当然为了保证访问数据的安全性,也可以使用线程锁或者join的方式进行保障,但是这样都会牺牲访问时间,并不是可选方案。那么通过ThreadLocal的变量副本方式,即可用牺牲内存的方式来换取并发的提高(空间换时间)。
下面通过修改上面工厂类代码模拟并发访问单例实体时的空间换时间方式。
ThreadLocal的基本原理是对并发下访问的对象创建副本,通过ThreadLocal的实例作为key,被访问对象作为value的形式进行存储。详细的ThreadLocal介绍可以看这篇文章。
下面修改单例工厂类,进行测试。
改写的实体类单例工厂
public class StudentThreadLocalFactory {
private static final ThreadLocal localStudent = new ThreadLocal<>();
private StudentThreadLocalFactory() {}
//改写单例模式获取对象信息
public static Student getStudent(){
Student student = (Student)localStudent.get();
if(student == null){
synchronized(StudentThreadLocalFactory.class){
if(student == null){
student = new Student();
localStudent.set(student);
}
}
}
return student;
}
}
将TraThread01、TraThread02和TestThread中获取实体对象的方法切换到新的工厂类
Student stu = StudentThreadLocalFactory.getStudent();
重新执行测试类TestThread中main方法,得到如下结果:
线程一访问到的对象信息—null:null——com.study.pojo.Student@4a6382ee
Thread 2 get the object info—null:null——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—abc0:English0——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三0:中文备注0——com.study.pojo.Student@4a6382ee
线程一访问到的对象信息—张三1:中文备注1——com.study.pojo.Student@4a6382ee
Thread 2 get the object info—abc1:English1——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—abc2:English2——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三2:中文备注2——com.study.pojo.Student@4a6382ee
Thread 2 get the object info—abc3:English3——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三3:中文备注3——com.study.pojo.Student@4a6382ee
Thread 2 get the object info—abc4:English4——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三4:中文备注4——com.study.pojo.Student@4a6382ee
Thread 2 get the object info—abc5:English5——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三5:中文备注5——com.study.pojo.Student@4a6382ee
Thread 2 get the object info—abc6:English6——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三6:中文备注6——com.study.pojo.Student@4a6382ee
Thread 2 get the object info—abc7:English7——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三7:中文备注7——com.study.pojo.Student@4a6382ee
线程一访问到的对象信息—张三8:中文备注8——com.study.pojo.Student@4a6382ee
Thread 2 get the object info—abc8:English8——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三9:中文备注9——com.study.pojo.Student@4a6382ee
Thread 2 get the object info—abc9:English9——com.study.pojo.Student@72f4d50a
线程一访问到的对象信息—张三10:中文备注10——com.study.pojo.Student@4a6382ee
Thread 2 get the object info—abc10:English10——com.study.pojo.Student@72f4d50a
Thread 2 get the object info—abc11:English11——com.study.pojo.Student@72f4d50a
以上的结果可以看出,两个线程访问的对象地址不一样(具体的对象属性值信息也可以看到),那么对单例的对象操作就不会发生并发访问的问题。
Spring的IOC容器保证单例Bean并发访问时的安全性,基本上逻辑就是这样。