• 面试官:单例Bean一定不安全吗?实际工作中如何处理此问题?


    默认情况下,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 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、设计模式、消息队列等模块。

  • 相关阅读:
    DataGrip连接MySQL
    【LeetCode】最大连续 1 的个数
    【计算机网络】数据链路层:虚拟局域网
    Towards a General Purpose CNN for Long Range Dependencies in ND
    Oracle RAC ASM磁盘组删除、添加磁盘
    将Servlet项目改为SSM项目
    主成分分析(PCA):揭秘数据的隐藏结构
    yum安装报错Error: rpmdb open failed
    面向智慧文博的知识图谱构建综述
    jvm常用锁失效的几个场景
  • 原文地址:https://www.cnblogs.com/vipstone/p/17958686