Skip to main content

Bean是线程安全的吗?

作者:程序员马丁

在线博客:https://open8gu.com

note

大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。

回答话术

首先,Spring 创建 Bean 的过程会加锁,因此 Spring 本身保证创建 Bean 的过程是线程安全的。基于这个前提,我们实际讨论的是 Spring 管理的 Bean 在使用过程中是否是线程安全的,这个问题取决于 Bean 的作用域、Bean 本身是否有状态,以及 Bean 本身是否提供了线程安全的操作机制

如果我们选择了多例这种作用域,并且每次访问的都从 Spring 容器里面获取 Bean,由于每个线程访问的都是独立的对象,因此肯定是线程安全的。

如果 Bean 本身没有状态(即没有任何属性)或者不可变(即所有的属性都不可以修改),那么 Bean 也可以认为是线程安全的。比如按事务脚本的风格编写的传统三层的架构中,DAO、Service 和 Controller 很少有可修改的变量,因此它们是线程安全的。

最后,如果 Bean 提供的操作方法能保证线程安全——比如加锁,或者使用 ThreadLocal 隔离不同线程操作的数据——那 Bean 显然也是线程安全的。比如 Spring 的 BeanFactory 就通过冻结配置和加锁来实现这个效果。

问题详解

1. Bean 的作用域

Spring 的 Bean 本身区分不同的作用域。

在默认情况下,我们交给 Spring 管理的 Bean 全部都是单例的,假如 Bean 是单例的,且有状态的,同时对状态的修改和访问没有任何同步机制来保证线程安全,那么它必定不是线程安全的

不过,如果 Bean 的作用域是单例的(signletion),或者我们自己注册了 SimpleThreadScope 指定了一个线程内的作用域,那么 Bean 同样是线程安全的,因为每个线程操作的 Bean 都是独立的

当然,即使作用域能保证线程安全,前提是正确的使用了作用域,比如:

@Component // 默认是单例的
public class Foo {}

@Component
public class Example {

@Autowired // 不是线程安全的,因为注入以后操作的一直都是同一个对象
private Foo injected;

@Autowired
private ApplicationContext context;

// 假设该方法被并发调用
public void run() {
injected.doSomething(); // 线程不安全,因为多线程操作的是同一个对象
context.getBean(Foo.class).doSomething(); // 线程安全,每次都获取一个新对象
}
}

2. Bean 本身是否有状态

Bean 本身是否有状态也是影响线程安全的重要因素。

首先,当我们说一个对象本身“是否有状态”,其实质上是指对象本身是否有可以修改的属性

  • 如果一个类,它没有任何状态(静态工具类),那么它肯定是线程安全的。
  • 如果一个对象,它的任何属性都是不可变的,那么它也是线程安全的。

基于这个逻辑,拿传统的的 Controller、Service 和 DAO 三层架构来说:

@Service
public class FooService {

@Autowired
private DAO dao;

@Transactional
private void save(Foo foo) {
dao.save(foo);
}
}

@Controller
public class FooController {

@Autowired
private FooService fooService;

@PostMapping
private void save(Foo foo) {
fooService.save(foo);
}
}

上述代码中,整个调用链路中几乎没有涉及到什么同时兼具业务逻辑和可修改属性的有状态对象。

实际上,如果 DAO 提供静态方法,整个 Controller 和 Service 层我们甚至都可以改成纯静态方法。这种面向过程风格的编码风格在 DDD 中被称为事务脚本,连有状态的对象都没有,自然也不需要担心线程安全问题

这也恰好解释了为什么 Spring 不推荐使用 Setter 方法注入,而大力推荐构造器注入,因为在这种方式下,我们可以将注入的成员变量也设置为不可变的,这样就彻底杜绝的最后的一点风险:

@Service
public class FooService {

private final DAO dao;

@Autowired
public FooService(DAO dao) {
this.dao = dao;
}

@Transactional
private void save(Foo foo) {
dao.save(foo);
}
}

@Controller
public class FooController {

private final FooService fooService;

@Autowired
public FooController(FooService fooService) {
this.fooService = fooService;
}

@PostMapping
private void save(Foo foo) {
fooService.save(foo);
}
}

严格来说,数据的状态不会消失而只会转移,在这种事务脚本里面,实际上所有的状态都转移到了数据库、缓存和消息队列里。

另外顺带一提,不可变对象这种设计本身就能很好的保证线程安全,这也是 JDK 的高版本引入不可变集合和值对象的一部分原因。

3. Bean 本身是否提供同步机制

即使是全局共享的有状态单例 Bean,并且也提供了可以被外部调用的修改状态的方法,只要 Bean 本身有足以保证线程安全的同步机制,那它依然是线程安全的。

举一个最简单粗暴的例子,Spring 的 BeanFactory,它本身就是一个需要在多线程环境下访问的有状态的 Bean,当我们调用它的 getBean 方法时,Spring 通过冻结配置使其配置不可变,并且在操作 Bean 时加锁,因此 BeanFactory 可以说是线程安全的。

又或者,我们将需要修改的变量放到 ThreadLocal 里面,将每个线程访问和操作的变量分开,这样也能做到线程安全。