Spring Boot线程安全指南

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框架不会为您解决它。您需要选择一种可能的解决方案:

  • synchronized 关键字和锁定-此选项使您可以访问同步的最大控制,但还需要更深入的了解在并行环境中使用的机制
  • 原子变量 - 您可以在Java标准库中找到一小组线程安全类型。该包中的类型可以安全地用作共享有状态bean中的字段。
  • 并发集合 - 除了原子变量之外,Java还为我们提供了一些有用的集合,我们可以使用它们而不必担心并发访问问题。

但请注意:无论您选择哪种方法,访问同步始终会对性能产生影响。如果您有其他选择,请尽量避免使用它。

在Spring组件中实现线程安全的方法​​​​​​​
正如我们已经讨论过的,Spring本身并没有解决并发访问的问题。如果bean的范围不是线程安全的,但其方法包含一些您总是希望安全运行的关键代码,请在该方法上使用synchronized关键字。


结论
我们需要知道Spring框架在多线程环境中的情况。必须自行提供线程安全性时的保障。

banq:其实可变数据或状态都是保存数据库,如果将数据库作为业务核心,就不必担心多线程问题,但是六边形和干净架构中,需要将数据库作为技术放到业务核心之外,在这种架构下,就需要多注意多线程问题。在普通场景下多线程问题基本由数据库技术解决了。本文问题只适合作为面试问答。