logo头像

不忘初心 方得始终

深入理解Spring

引言

Spring在当前Java项目开发中可谓是核心,无论是SpringMVC、SpringBoot、SpringCloud都离不开Spring这个核心。自己对Spring的了解也停留在一知半解状态。

说起Spring,都知道是一个轻量级开源企业开发框架,包含控制反转(IOC)和面向切面(AOP)两大特性。

IOC

什么是IOC(DI)

  IOC(Inversion Of Control,控制反转)。对于Spring框架来说,就是由Spring来负责控制对象的生命周期和对象间的关系。在一个对象中,如果要使用其他的对象,就必须得到它(自己new一个或者JNDI中查询一个),使用完对象之后还要将对象销毁(比如Connection等),对象始终会和其他的接口或类耦合起来。

  所有的类都会在Spring容器中登记,告诉Spring自己是什么,需要什么,然后Spring会在系统运行到适当的时候,把你需要的东西主动给你,同时也把你交给其他需要你的东西。所有类的创建、销毁都由Spring来控制,也就是说控制对象生命周期的不再是引用它的对象,而是Spring容器。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被Spring控制,所以这叫做控制反转。

  IOC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。比如对象A需要操作数据库,本来需要在A中自己编写代码来获得一个Connection对象,有了Spring我们就只需要告诉Spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,Spring会在适当的时候制造一个Connection,然后像打针一样注射到A中,这样就完成了各个对象之间关系的控制。A需要依赖Connection才能正常运行,而这个Connection是由Spring注入到A中的,依赖注入的名字就是这么来的。那么DI是如何实现的呢?Java1.3之后一个重要特征就是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,Spring就是通过反射来实现注入的。

  在没有使用Spring的时候,每个对象在需要使用他的合作对象时,自己均要使用像new object() 这样的语法来将合作对象创建出来,这个合作对象是由自己主动创建出来的,创建合作对象的主动权在自己手上,自己需要哪个合作对象,就主动去创建,创建合作对象的主动权和创建时机是由自己把控的,而这样就会使得对象间的耦合度高了,A对象需要使用合作对象B来共同完成一件事,A要使用B,那么A就对B产生了依赖,也就是A和B之间存在一种耦合关系,并且是紧密耦合在一起,而使用了Spring之后就不一样了,创建合作对象B的工作是由Spring来做的,Spring创建好B对象,然后存储到一个容器里面,当A对象需要使用B对象时,Spring就从存放对象的那个容器里面取出A要使用的那个B对象,然后交给A对象使用,至于Spring是如何创建那个对象,以及什么时候创建好对象的,A对象不需要关心这些细节问题(你是什么时候生的,怎么生出来的我可不关心,能帮我干活就行),A得到Spring给我们的对象之后,两个人一起协作完成要完成的工作即可。

  所以控制反转IOC(Inversion Of Control)是说创建对象的控制权进行转移,以前创建对象的主动权和创建对象的时机是由自己把控的,而现在这种权利转移到了第三方。比如交给了IOC容器,它就是一个专门来创建对象的工厂,你需要什么对象,它就给你什么对象,有了IOC容器,依赖关系就变了,原来的依赖关系就没了,他们都依赖IOC容器,通过IOC容器来建立它们之间的关系。

IOC的优点

可维护性比较好

  便于进行单元测试,便于调试程序和诊断故障。代码中的每一个Class都可以单独测试,彼此之间互不影响,只要保证自身的功能无误即可,这就是组件之间低耦合或者无耦合带来的好处。

可复用性好

  可以把具有普遍性的常用组件独立出来,反复利用到项目中的其它部分,或者是其它项目,当然这也是面向对象的基本特征。IOC不仅更好地贯彻了这个原则,提高了模块的可复用性。

分层、解耦

  IOC生成对象的方式转为外置方式,也就是把对象生成放在配置文件里进行定义,这样,当我们更换一个实现子类将会变得很简单,只要修改配置文件就可以了,完全具有热插拨的特性。

AOP

  AOP(Aspect-OrientedProgramming,面向切面编程)。例如日志功能,日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

  而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即切面。所谓“切面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系。

  使用“横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。Aop 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

  实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入(静态代理)的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“Aspect”的代码。

  AOP的优点:便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。

具体代理模式可参见Spring之动态代理

Spring单例、线程安全

Spring中的线程安全

  Spring中管理bean实例默认情况下都是单例的【singleton】,还有prototype类型,按其作用域来讲有singleton、prototype、request、session、global、session。

  Spring中的单例与设计模式里面的单例略有不同,设计模式的单例是在整个应用中只有一个实例,而Spring中的单例是在一个IOC容器中就有一个实例。但Spring中的单例也不会影响应用的并发访问【不会出现各个线程之间的等待问题或是死锁问题】,因为大多数时候客户端都在访问我们应用中的业务对象,而这些业务对象并没有做线程的并发限制,只是在这个时候我们不应该在业务对象中设置那些容易出错的成员变量,在并发访问的时候这些成员变量将会是并发线程中的共享对象,那么这个时候就会出现意外情况。

  实体bean不是单例的,并没有交给Spring来管理,每次我们要手动new出来的【Item item = new Item()】,所以即使是那些处理我们提交数据的业务处理类是被多线程共享的,但是他们处理的数据并不是共享的,数据是每一个线程都有自己的一份,所以在数据这个方面是不会出现线程同步方面的问题的。但是那些在Dao中的xxxDao或Controller中的xxxService,这些对象都是单例的,那么就会出现线程同步的问题。但是这些对象虽然会被多个线程并发访问,可我们访问的是他们里面的方法,这些类里面通常不会含有成员变量,Dao里面的ibatisDao是框架里面封装好的,已经被测试,不会发生线程同步问题。所以出现问题的地方就是我们自己系统里面的业务对象,所以一定要注意这些业务对象里面千万不能独立成员变量,否则会出问题

  Spring中容器托管的类如果没有特殊声明(scope=“prototype”),则默认为单例模式。当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程并发执行该请求多对应的业务逻辑(成员方法)。此时就要注意,如果该处理逻辑中有对该单列状态的修改(成员变量),则必须考虑线程同步问题;否则由于在业务逻辑中执行所需的局部变量会分配在栈空间中,所以不需要同步。

  其实函数本身是代码,代码是只读的,无论多少个线程同时调都无所谓(因为只是读的),但是函数中肯定是要用到数据的,如果数据是函数参数、局部变量,那么这些数据都是存在每个线程自己的栈上的,同时调用是没有关系的,不会涉及到线程安全资源共享的问题。 但是如果使用到了全局静态变量或者类的成员变量的时候。就会出现数据安全的问题,还有,如果我们的成员变量在函数体内如果只进行读操作,不进行写操作,也是线程安全的。

  总而言之,单例的方法在同一个时刻是可以被多个线程同时调用的。在写程序的时候要尽可能少的使用类的成员变量,如果使用成员变量,尽量保证只对成员变量进行读操作。

当很多用户去修改自己信息的时候,用户线程会通过调用dao(dao都是给注入有配链接池的数据源的),dao会拿到链接池中的一个链接,将我们要处理的信息(SQL语句等)交付给数据库,数据库会按照自己的多线程处理机制完成线程的同步,然后进行数据安全处理,线程在完成数据处理后会将占有的链接放回到链接池中。

Spring中同步机制

  单例模式的意思就是只有一个实例。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。

  当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求多对应的业务逻辑(成员方法),此时就要注意了,如果该处理逻辑中有对该单列状态的修改(体现为该单列的成员属性),则必须考虑线程同步问题。

同步机制比较

ThreadLocal线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

  在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

  而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用 。

  概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

Spring使用ThreadLocal解决线程安全问题

  在一般情况下,只有无状态的Bean才可以在多线程环境下共享。在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolderTransactionSynchronizationManagerLocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,这样有状态的Bean就可以在多线程中共享了。

  一般的Web应用划分为表现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

线程安全问题都是由成员变量引起的

  若每个线程中对成员变量(全局变量、静态变量)只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
1) 常量始终是线程安全的,因为只存在读操作。
2)每次调用方法前都新建一个实例是线程安全的,因为不会访问共享的资源。
3)局部变量是线程安全的。因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。局部变量包括方法的参数变量和方法内变量。
  有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。
  无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。

  无状态的Bean适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式。Prototype: 每次对bean的请求都会创建一个新的bean实例。

总结

  • Spring中DAO和Service都是以单实例的bean形式存在,Spring通过ThreadLocal类将有状态的变量(例如数据库连接Connection)本地线程化,从而做到多线程状况下的安全。在一次请求响应的处理线程中, 该线程贯通展示、服务、数据持久化三层,通过ThreadLocal使得所有关联的对象引用到的都是同一个变量。
  • 在事务属性为REQUIRED时,在相同线程中进行相互嵌套调用的事务方法工作于相同的事务中。如果互相嵌套调用的事务方法工作在不同线程中,则不同线程下的事务方法工作在独立的事务中。
  • 程序只要使用SpringDAO模板,例如JdbcTemplate进行数据访问,一定没有数据库连接泄露问题!如果程序中显式的获取了数据连接Connection,则需要手工关闭它,否则就会泄露!
  • 当Spring事务方法运行时,就产生一个事务上下文,它在本事务执行线程中对同一个数据源绑定了一个唯一的数据连接,所有被该事务上下文传播的方法都共享这个连接。要获取这个连接,如要使用Spirng的资源获取工具类DataSourceUtils。
  • 事务管理上下文就好比一个盒子,所有的事务都放在里面。如果在某个事务方法中开启一个新线程,新线程中执行另一个事务方法,则由上面第二条可知这两个方法运行于两个独立的事务中,但是:如果使用DataSourcesUtils,则新线程中的方法可以从事务上下文中获取原线程中的数据连接!

参考

Spring容器深入

Spring单例、线程安全、事务等疑惑 收集

微信打赏

赞赏是不耍流氓的鼓励