Spring控制器/服务/单单例是线程安全的吗?
答案是:它取决于。 决定组件线程安全性的主要因素是其作用域Scope。
哪个Spring作用域是线程安全的?
为了回答这个问题,首先需要了解Spring何时创建新线程。
在基于servlet的标准Spring Web应用程序中,每个新的HTTP请求都会生成一个新线程。 如果容器为特定请求创建一个新的bean实例,我们可以说这个bean是线程安全的 。
让我们来看一下Spring中的作用域,并关注容器何时创建它们。
Spring单例线程安全吗?
简短的回答是:不
这是因为单例Bean的生命周期很长。这些bean可能会在来自不同用户的许多HTTP请求中反复使用。如果不使用 @Lazy ,框架会在应用程序启动时创建唯一的一个bean实例, 并确保使用者会自动连接并重用相同的这个实例。只要容器存在,这个单例Bean实例一直会存在。
但框架并不控制单例的使用方式。如果两个不同的线程同时执行单例的方法,则不能保证两个调用都将同步并在能顺序运行。(需要synchronize等锁才能实现同步)
换句话说, 您有责任 确保您的代码在多线程环境中安全运行。Spring不会为你做这事。
请求级别作用域Request scope
如果你想确保你的bean是线程安全的,你应该使用 @RequestScope, 顾名思义,Spring将这种bean实例绑定到特定的Web请求。
这种bean实例不在多个线程之间共享,因此您不必关心并发。
但是等一下。
如果这种bean的并发很大,创建bean的新实例就比重用现有实例要慢。这时候,使用单例Bean,除非你有一个真正的用例场景可以使用RequestScope的bean。
会话级别作用域
Spring将会话bean与特定用户关联。当新用户访问您的应用程序时,将创建一个新的会话Bean实例,并为该用户的所有请求重用该实例。
如您所知,某些用户的请求可能是并发的。因此,会话bean不是线程安全的。它们的生命周期比请求作用域bean长。多个请求可以同时调用同一个会话bean。
prototype Bean
我把原型范围作为最后讨论的范围,因为我们无法清楚地说它始终是线程安全的。 Prototype的线程安全性取决于包含原型的bean的作用域。
只要使用者需要这个Bean的实例,Spring就会根据需要创建原型bean。(类似new object一样调用一次创建一次);
想象一下,你的应用程序中有两个bean。一个是单例Bean,第二个是请求作用域的bean。两者都依赖于第三个原型的bean。
让我们先考虑单例bean:因为单例不是线程安全的,所以对其原型方法的调用也可以同时运行。当多个线程共享单例时,Spring注入该单例的原型的单个实例也将被共享。
对于请求作用域的bean:Spring为每个Web请求创建此类组件的新实例。每个请求都绑定到一个单独的线程。因此,请求bean的每个实例都获得自己的原型bean实例。在这种情况下,您可以将原型视为线程安全的。
那么Spring Web控制器是否是线程安全的?
这取决于这种控制器的作用域。
如果将控制器定义为默认的单例bean,则它不是线程安全的。将默认作用域更改为会话级别的,也不会使控制器安全。但是,请求作用域将使控制器bean安全地用于并发Web请求。
如果将控制器定义为原型bean,因为我们从不将控制器注入其他Bean,它们是我们应用程序的入口点。那么当您将控制器定义为原型bean时,Spring的行为如何?
当您将控制器定义为原型时,Spring框架将为每个Web请求创建一个新实例 。除非将它们注入不安全的作用域bean,否则可以将原型作用域的控制器视为线程安全的。
如何使任何Spring bean线程安全?
可以做的最好的办法是解决访问同步问题。
怎么做?
使您的bean类变成无状态。(banq注:又回到了EJB的无状态bean和有态Bean,无状态实际是不可变)
如果bean的方法执行不修改其实例的字段属性,则bean是无状态的。
更改方法内的局部变量是完全可以的,因为对方法的每次调用都会为这些变量分配内存。与在所有非静态方法之间共享的实例字段不同。
完美的无状态bean没有字段,但你不会经常看到这样的实用程序类。通常,您的bean有一些字段。但是通过应用一些简单的规则,您可以使任何bean无状态且线程安全。
如何使Spring bean无状态?
将所有bean字段设置为final,以指示在bean字段的生命周期中不应再次重新分配。
但是不要将字段修改与重新分配混淆! 使所有bean的字段final不会使它成为无状态。 如果在运行时期间可以更改分配给bean的最终字段的值,则此类bean仍然不是线程安全的。
比如使用final String, 无法更改String字段的值,String类是不可变的,就像Integer,Boolean和其他原始包装器一样。在这种情况下,您还可以安全地使用基本类型。但是更复杂的对象如Collection,Map或自定义数据类呢?
对于像集合这样的常见类型,您可以使用标准Java库中可以找到的不可变实现。您可以使用Java 9中添加的工厂方法轻松创建不可变集合。如果您仍使用旧版本,请不要担心。您还可以 在Collections类中 找到转换方法,如 unmodifiableList() 。
如果涉及自定义数据类型,则必须确保它们是不可变的。在Java中创建不可变类超出了本文的范围。(banq注:业务类型尽量使用值对象)
有状态Spring bean中的线程安全变量
无状态bean听起来像银弹。但是,如果您已经拥有有状态bean并且必须在其中一个字段上同步访问权限呢?
在这种情况下,您有一个经典的Java问题,即对类字段的并发修改访问。Spring框架不会为您解决它。您需要选择一种可能的解决方案:
但请注意:无论您选择哪种方法, 访问同步始终会对性能产生影响 。如果您有其他选择,请尽量避免使用它。
在Spring组件中实现线程安全的方法
正如我们已经讨论过的,Spring本身并没有解决并发访问的问题。如果bean的范围不是线程安全的,但其方法包含一些您总是希望安全运行的关键代码,请在该方法上使用 synchronized 关键字。
结论
您应该知道Spring框架在多线程环境中的位置。您了解了组件的范围如何影响其安全性以及必须自行提供线程安全性时的选项。