线程安全-Spring Bean 作用域类型(Scope)
16:53:00 2020-06-12
今天上课学习MyBatis时,在老师讲到SqlSession是线程不安全的时候,提到这样一句话:“SqlSession是单线程对象,因为它是非线程安全的,是持久化操作的独享对象,类似JDBC中的Connection,底层就封装了JDBC连接”。我当时就感到疑惑:因为我习惯从字面上去理解一个东西,Connection译为连接,连接之后再开启一个会话,这个会话是独享的我可以理解,而当他说Connection也是独享的时候我就犯懵了,后来想清楚了,JDBC中的Connection和SqlSession其实也就是一回事,如果我是JDBC驱动的设计人员,我就会把Connection改为Session。
这几天一直是围绕着如何搭建一个Spring+SpringMVC+MyBatis框架的Web项目,对SpringMVC做了挺多的学习,也了解了一些基础的知识,这里,我就将看过的博客文章,自己觉得写得不错,结合自己的理解,翻新一遍,整合到自己的博客里来。
一. Spring Bean 的作用域类型
- Singleton 单例,默认作用域。
- Prototype 原型,每次创建一个新实例。
- Request 请求,每次 Http 请求创建一个新实例,适用于 WebApplication 环境下。
- Session 会话,同一个会话共享一个实例,不同会话使用不同的实例。
- Global-session 全局会话,所有会话共享一个实例。
二. Spring 单例模式及线程安全
Spring 框架中的 Bean,或者说组件,获取实例的时候都是默认单例模式。
单例模式的定义:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。无状态的 Bean 适合用单例模式(无状态就是一次操作,不能保存数据,无状态对象就是没有实例变量的对象),这样可以共享实例,提高性能;有状态的 Bean ,多线程环境下不安全,适合用 Prototype 原型模式,每次对 Bean 的请求都会创建一个新的实例(有状态就是有数据存储功能,有状态对象就是有实例变量的对象,可以保存数据)。
三. 线程安全的问题
当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑(成员方法),此时就要注意,如果该处理逻辑中有对单例状态的修改(体现为改单例的成员属性),则必须考虑线程同步的问题。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的,或者说:一个类或者程序所提供的接口对于线程来说是原子性操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,这样也叫做线程安全。
说来说去,线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,没有写操作,一般来说,这恶全局变量就是线程安全的;若多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
有三种情况是线程安全的(不限于此):
- 常量始终是线程安全的,因为只存在读操作。
- 每次调用方法前都新建一个实例是线程安全的,因为不会存在访问共享的资源。
- 局部变量是线程安全的,因为每执行一个方法,都会在各自的方法栈里头创建局部变量,它不是共享的资源,局部变量包括方法的参数变量和方法内声明的变量。
四. 线程安全的实现方式
1. 在同步机制,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享,使用同步机制要求程序缜密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计的难度相对较大。
2. ThreadLocal 从另一个角度来解决多线程的并发访问,ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突,因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了,ThreadLocal 提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进 ThreadLocal。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,ThreadLocal 采用了“以空间换时间”方式,前者仅提供一个变量,让不同的线程排队访问,后者为每一个线程都提供了一个变量,因此可以同时访问而互不影响。
线程安全案例
SimpleDateFormat(下面简称 sdf)类内部有一个 Calendar 对象引用,它用来存储和这个 sdf 相关的日期信息,例如 sdf.parse(dateStr). sdf.format(date) 诸如此类的方法参数传入的日期信息相关的 String, Date 等等,都是交由 Calendar 引用来存储的,这样就会导致一个问题,如果你的 sdf 是静态的,那么多个 Thread 之间就会共享这个 sdf,同时共享这个 Calendar 引用,可见是非线程安全的,根据前面的状态的定义,这是一个有状态的对象。
对策:
1. 使用 synchronized 关键字进行数据同步,或者使用 ThreadLocal 保证线程安全。
2. 不使用 JDK 自带的时间格式化类,使用其他替代:
-
- 使用 Apache Commons 里的 FastDateFormat,宣称是又快又线程安全的 SimpleDateFormat,可惜它只能对日期进行格式化,不能对日期串进行解析。
- 使用 Joda-Time 类库来处理时间相关问题,该类库对时间的处理方式比较完美。