列表内容
我们知道Spring相对于之前框架的明显一点区别就是Spring容器生成的Bean都是默认单例的,初读到这里的时候我也是有点疑惑,所以写这篇文章来谈谈线程安全和单例多例的问题。
在讲单例和线程安全之前首先我们要明白一点,那就是在单例模式下多线程也可以同时访问同一个对象。单例模式顾名思义,所有的线程来调用方法的时候都是由同一个实例对象来调用方法的,那么如果设计一个类的实现为单例,当多个线程调用方法时会不会出现线程安全问题呢?答案是不会,为什么呢,其实很简单,我们可以从线程在内存中的存储作为出发点,当线程开启时,JVM会在栈和方法去中对该线程开启私有的存储空间,也就是说线程由独立的存储空间,但是不同线程要共享堆中的资源,堆中有什么呢,是我们new出来的对象数组等。
结合Spring来理解,Spring的Bean默认都是单例的,那么会不会有线程安全的问题,要知道线程安全是针对不同用户之间对成员变量的操作互相影响导致的,换句话说如果你的类中没有非静态成员变量 ,即使有多个线程同时访问单例对象的成员方法也不会出问题,因为每个线程在栈和方法区中会有私有的存储空间,并不会导致数据混乱出现类似事务ACID的问题,写到这里大家可能已经明白了,线程安全只是针对全局变量的,如果是方法的话用同一个对象同时进行调用也不会出现安全问题。Service和DAO的实例对象大多数情况下只是作一个方法的调用,在Service的实现类中如果有
全局变量怎么办呢?
先看下面小段代码,一个controller,一个service。
controller.java代码:
………
@Autowired
private XXXService xxxService;
………
@RequestMapping(“/doXXX.do”)
public void doXXX(){
……
xxxService.saveXXX(String content,….);
……
}
XXXService.java代码:
private String content;
……
private void init(){//清空请求参数
content = null;
……
}
public boolean saveXXX(String content, ……){
this.init(content, …);
this.content = content;
//业务逻辑处理
}
以上这段代码在访问量不构成并发时不会出现什么问题。但当一个请求还未完成,另一个请求已经开始执行的情况下就会出现问题(并发):第二个请求执行执行init()方法会将第一个请求的content变量设置为null或它本身的值,这样数据就被篡改了。
编码者这样写的目的是因为content等变量需要在多个方法中使用,而且变量很多,但又不想通过方法参数的方式来传递,故使用成员变量。
先看看为什么会出现这种情况。由于系统采用springmvc框架,springmvc核心控制器DispatcherServlet 默认为每个controller生成单一实例来处理所有用户请求,所以在这个单一实例的controller中,它的XXXService也是一个实例处理所有请求,这样XXXService的成员变量就被所有请求共享。这样就会出现并发请求时变量内容被篡改的问题。
那么出现这种问题如何解决呢?
第一种方式:既然是全局变量惹的祸,那就将全局变量都编程局部变量,通过方法参数来传递。
第二种方式: jdk提供了java.lang.ThreadLocal,它为多线程并发提供了新思路。 (当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本)
那么在什么地方使用ThreadLocal呢?什么变量是请求公用的就将该变量托付给ThreadLocal来管理其线程副本,所以我们在xxxService中使用它。
XXXService.java代码:
private ThreadLocal contentTL = new ThreadLocal();
//private String content;使用contentTL代替content;
……
public boolean saveXXX(String content, ……){
this.contentTL.set(content);
//业务逻辑处理
//在各方法中使用content时候用this.contentTL.get()代替
1
2
}
此类并发篡改数据的问题,可以在开发工具中设置断点调试的方式来模拟并发。即第一次请求运行到断点时,查看content内容,并且不让程序继续往下运行,同时再发起一个请求,查看content内容。如内容是第一次请求的内容,并且让第一个请求跑完后,第二个请求到断线处的content正确时,可以确定不会出现并发问题。
也就是说,如果一个类设计成单例模式, 那么就要注意他的全局变量,如果变量这里没有问题的话方法的调用也不会存在安全问题,因为线程会同时调用同一个对象的方法,但是在内存中有自己独立的空间作为存储,Spring设计成单例Bean主要是为了节省没必要的资源浪费。