默认情况下,Spring Boot 中的 Bean 是非线程安全的。这是因为,默认情况下 Bean 的作用域是单例模式,那么此时,所有的请求都会共享同一个 Bean 实例,这意味着这个 Bean 实例,在多线程下可能被同时修改,那么此时它就会出现线程安全问题。

Bean 的作用域(Scope)指的是确定在应用程序中创建和管理 Bean 实例的范围。也就是在 Spring 中,可以通过指定不同的作用域来控制 Bean 实例的生命周期和可见性。例如,单例模式就是所有线程可见并共享的,而原型模式则是每次请求都创建一个新的原型对象。

1.单例Bean一定不安全吗?

并不是,单例 Bean 分为以下两种类型:

  1. 无状态 Bean(线程安全):Bean 没有成员变量,或多线程只会对 Bean 成员变量进行查询操作,不会修改操作。
  2. 有状态 Bean(非线程安全):Bean 有成员变量,并且并发线程会对成员变量进行修改操作。

所以说:有状态的单例 Bean 是非线程安全的,而无状态的 Bean 是线程安全的

但在程序中,只要有一种情况会出现线程安全问题,那么它的整体就是非线程安全的,所以总的来说,单例 Bean 还是非线程安全的。

① 无状态的Bean

无状态的 Bean 指的是不存在成员变量,或只有查询操作,没有修改操作,它的实现示例代码如下:

import org.springframework.stereotype.Service;

@Service
public class StatelessService {
public void doSomeTask() {
// 执行任务
}
}

② 有状态的Bean

有成员变量,并且存在对成员变量的修改操作,如下代码所示:

import org.springframework.stereotype.Service;

@Service
public class UserService {
private int count = 0;
public void incrementCount() {
count++; // 非原子操作,并发存在线程安全问题
}
public int getCount() {
return count;
}
}

2.如何保证线程安全?

想要保证有状态 Bean 的线程安全,可以从以下几个方面来实现:

  1. 使用 ThreadLocal(线程本地变量):每个线程修改自己的变量,就没有线程安全问题了。
  2. 使用锁机制:例如 synchronized 或 ReentrantLock 加锁修改操作,保证线程安全。
  3. 设置 Bean 为原型作用域(Prototype):将 Bean 的作用域设置为原型,这意味着每次请求该 Bean 时都会创建一个新的实例,这样可以防止不同线程之间的数据冲突,不过这种方法增加了内存消耗。
  4. 使用线程安全容器:例如使用 Atomic 家族下的类(如 AtomicInteger)来保证线程安全,此实现方式的本质还是通过锁机制来保证线程安全的,Atomic 家族底层是通过乐观锁 CAS(Compare And Swap,比较并替换)来保证线程安全的。

具体实现如下。

① 使用ThreadLocal保证线程安全

实现代码如下:

import org.springframework.stereotype.Service;

@Service
public class UserService {
private ThreadLocal<Integer> count = ThreadLocal.withInitial(() -> 0); public void incrementCount() {
count.set(count.get() + 1);
} public int getCount() {
return count.get();
}
}

使用 ThreadLocal 需要注意一个问题,在用完之后记得调用 ThreadLocal 的 remove 方法,不然会发生内存泄漏问题。

② 使用锁机制

锁机制中最简单的是使用 synchronized 修饰方法,让多线程执行此方法时排队执行,这样就不会有线程安全问题了,如下代码所示:

import org.springframework.stereotype.Service;

@Service
public class UserService {
private int count = 0;
public synchronized void incrementCount() {
count++; // 非原子操作,并发存在线程安全问题
}
public int getCount() {
return count;
}
}

③ 设置为原型作用域

原型作用域通过 @Scope("prototype") 来设置,表示每次请求时都会生成一个新对象(也就没有线程安全问题了),如下代码所示:

import org.springframework.stereotype.Service;

@Service
@Scope("prototype")
public class UserService {
private int count = 0;
public void incrementCount() {
count++; // 非原子操作,并发存在线程安全问题
}
public int getCount() {
return count;
}
}

④ 使用线程安全容器

我们可以使用线程安全的容器,例如 AtomicInteger 来替代 int,从而保证线程安全,如下代码所示:

import org.springframework.stereotype.Service;
import java.util.concurrent.atomic.AtomicInteger; @Service
public class UserService { private AtomicInteger count = new AtomicInteger(0); public void incrementCount() {
count.incrementAndGet();
} public int getCount() {
return count.get();
}
}

实际工作中如何保证线程安全?

实际工作中,通常会根据具体的业务场景来选择合适的线程安全方案,但是以上解决线程安全的方案中,ThreadLocal 和原型作用域会使用更多的资源,占用更多的空间来保证线程安全,所以在使用时通常不会作为最佳考虑方案。

而锁机制和线程安全的容器通常会优先考虑,但需要注意的是 AtomicInteger 底层是乐观锁 CAS 实现的,因此它存在乐观锁的典型问题 ABA 问题(如果有状态的 Bean 中既有 ++ 操作,又有 -- 操作时,可能会出现 ABA 问题),此时就要使用锁机制,或 AtomicStampedReference 来解决 ABA 问题了。

小结

单例模式的 Bean 并不一定都是非线程安全的,其中有状态的 Bean 是存在线程安全问题的。实际工作中通常会使用锁机制(synchronized 或 ReentrantLock)或线程安全的容器来解决 Bean 的线程安全问题,但具体使用哪种方案,还要结合具体业务场景来定。

本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

面试官:单例Bean一定不安全吗?实际工作中如何处理此问题?的更多相关文章

  1. Spring IOC 容器源码分析 - 获取单例 bean

    1. 简介 为了写 Spring IOC 容器源码分析系列的文章,我特地写了一篇 Spring IOC 容器的导读文章.在导读一文中,我介绍了 Spring 的一些特性以及阅读 Spring 源码的一 ...

  2. 【Spring源码分析】非懒加载的单例Bean初始化过程(上篇)

    代码入口 上文[Spring源码分析]Bean加载流程概览,比较详细地分析了Spring上下文加载的代码入口,并且在AbstractApplicationContext的refresh方法中,点出了f ...

  3. Spring源码分析:非懒加载的单例Bean初始化过程(上)

    上文[Spring源码分析]Bean加载流程概览,比较详细地分析了Spring上下文加载的代码入口,并且在AbstractApplicationContext的refresh方法中,点出了finish ...

  4. Spring源码解析之八finishBeanFactoryInitialization方法即初始化单例bean

    Spring源码解析之八finishBeanFactoryInitialization方法即初始化单例bean 七千字长文深刻解读,Spirng中是如何初始化单例bean的,和面试中最常问的Sprin ...

  5. 单例 Bean 的线程安全问题

    最近面试遇到一个问题:单例 Bean 的线程安全问题怎么解决的. 之前了解但是没有深究它的解决方法.大部分时候我们并没有在项目中使用多线程,所以很少有人会关注这个问题. 大部分 Bean 实际都是无状 ...

  6. 5.2:缓存中获取单例bean

    5.2  缓存中获取单例bean 介绍过FactoryBean的用法后,我们就可以了解bean加载的过程了.前面已经提到过,单例在Spring的同一个容器内只会被创建一次,后续再获取bean直接从单例 ...

  7. 【Spring源码分析】非懒加载的单例Bean初始化过程(下篇)

    doCreateBean方法 上文[Spring源码分析]非懒加载的单例Bean初始化过程(上篇),分析了单例的Bean初始化流程,并跟踪代码进入了主流程,看到了Bean是如何被实例化出来的.先贴一下 ...

  8. 【Spring源码分析】非懒加载的单例Bean初始化前后的一些操作

    前言 之前两篇文章[Spring源码分析]非懒加载的单例Bean初始化过程(上篇)和[Spring源码分析]非懒加载的单例Bean初始化过程(下篇)比较详细地分析了非懒加载的单例Bean的初始化过程, ...

  9. Spring IOC(三)单例 bean 的注册管理

    Spring IOC(三)单例 bean 的注册管理 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) 在 Spring 中 ...

  10. Spring IOC 容器源码分析 - 创建单例 bean 的过程

    1. 简介 在上一篇文章中,我比较详细的分析了获取 bean 的方法,也就是getBean(String)的实现逻辑.对于已实例化好的单例 bean,getBean(String) 方法并不会再一次去 ...

随机推荐

  1. SQL函数Intersect,except整理

    1.  集合函数使用的规则 ①   每个集合要求列数量相同,列顺序相同. ②   每个集合显示的列,要求数据类型一致或者可隐式转换成同一数据类型 ③   最终集合列名称与第一个集合的列名称一致  2. ...

  2. PPT太大发不出去?教你三个PPT压缩方法,200M的PPT变15M

    相信有很多小伙伴在工作的时候,都会制作不少的PPT,而我们也知道很多PPT在制作完成以后,体积就会变得非常大,在发送给别人的时候总是会受到限制,是有点难搞了. 别担心,今天小编将告诉大家三个简单的方法 ...

  3. poe不能用了poe.com收费了

    Anthropic's fastest model, with strength in creative tasks. Features a context window of 9k tokens ( ...

  4. linux特殊权限rws和rwt

    Linux文件,除了rwx这些权限外,还有一些特殊的权限,如rws.rwt. 1.s权限(setuid) 1.1 设置方法:chmod u+s 该位可以让普通用户以root用户的角色运行只有root帐 ...

  5. Mybatis_plus笔记

    Mybatis_plus笔记 在使用mybatis_plus的过程中我们可以明显的感受到他的强大之处.它就像是Mybatis和Jpa的结合体一样,它拥有jpa对单表的各种CRUD操作以及强大的条件构造 ...

  6. CSS色域、色彩空间、CSS Color 4新标准

    引言 近期,三大主流浏览器引擎均发布最新版本,支持W3C的CSS Color 4标准,包含新的取色方法color()和相应语法,可展示更多的色域及色彩空间,这意味着web端能展示更丰富更高清的色彩.虽 ...

  7. 一步步带你剖析Java中的Reader类

    本文分享自华为云社区<深入理解Java中的Reader类:一步步剖析>,作者:bug菌. 前言 在Java开发过程中,我们经常需要读取文件中的数据,而数据的读取需要一个合适的类进行处理.J ...

  8. VLAN通信之单臂路由与三层交换

    VLAN之间通信 再次提及,vlan是虚拟局域网,用于分隔广播域,解决广播风暴.但是vlan之间无法直接通信.所有我们要用三层交换.单臂路由来实现vlan之间的通信. 单臂路由 使用场景:规划错误,只 ...

  9. 2D物理引擎 Box2D for javascript Games 第七章 子弹和感应器

    2D物理引擎 Box2D for javascript Games 第七章 子弹和感应器 你知道 Box2D 可以在每一个时间步中管理刚体间的碰撞并决算它们. 总之,在愤怒的小鸟中制作攻城机器期间,发 ...

  10. Azure Data Factory(十)Data Flow 组件详解

    一,引言 随着大数据技术的不断发展,数据处理和分析变得越来越重要.为了满足企业对数据处理的需求,微软推出了 Azure Data Factory (ADF),它是一个云端的数据集成服务,用于创建.安排 ...