跳至主要內容

Spring系列八股3 - Bean2 (作用域 + 线程安全问题)

codejavaspring八股约 1454 字大约 5 分钟

Spring基础

Bean的作用域

单例模式 (默认)

Bean 的作用域决定了 Bean 实例的生命周期和创建策略,singleton 是默认的作用域

整个 Spring 容器中只会有一个 Bean 实例。不管在多少个地方注入这个 Bean,拿到的都是同一个对象

@Component  // 默认就是singleton
public class UserService {
  // 整个应用中只有一个UserService实例
}

生命周期和 Spring 容器相同,容器启动时创建,容器销毁时销毁

实际开发中,像 Service、Dao 这些业务组件基本都是单例的,因为单例既能节省内存,又能提高性能

多例模式

当把 scope 设置为 prototype 时,每次从容器中获取 Bean 的时候都会创建一个新的实例

@Component
@Scope("prototype")
public class OrderProcessor {
  // 每次注入或获取都是新的实例
}

当需要处理一些有状态的 Bean 时会用到 prototype,比如每个订单处理器需要维护不同的状态信息

注入问题

需要注意的是,在 singleton Bean 中注入 prototype Bean 时要小心,因为 singleton Bean 只创建一次,所以 prototype Bean 也只会注入一次。这时候可以用 @Lookup 注解或者 ApplicationContext 来动态获取

@Component
public class SingletonService {
  // 错误的做法,prototypeBean只会注入一次
  @Autowired
  private PrototypeBean prototypeBean;
  
  // 正确的做法,每次调用都获取新实例
  @Lookup
  public PrototypeBean getPrototypeBean() {
      return null;  // Spring会重写这个方法
  }
}

其他模式

除了 singleton 和 prototype,Spring 还支持其他作用域,比如 request、session、application 和 websocket

  • 如果作用于是 request,表示在 Web 应用中,每个 HTTP 请求都会创建一个新的 Bean 实例,请求结束后 Bean 就被销毁
  • 如果作用于是 session,表示在 Web 应用中,每个 HTTP 会话都会创建一个新的 Bean 实例,会话结束后 Bean 被销毁
  • application 作用域表示在整个应用中只有一个 Bean 实例,类似于 singleton,但它的生命周期与 ServletContext 绑定
  • websocket 作用域表示在 WebSocket 会话中每个连接都有自己的 Bean 实例。WebSocket 连接建立时创建,连接关闭时销毁

单例Bean会存在线程安全问题吗

首先要明确一点。Spring 容器本身保证了 Bean 创建过程的线程安全,也就是说不会出现多个线程同时创建同一个单例 Bean 的情况。

线程安全的创建过程主要靠 synchronized 实现

// 单例缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
  synchronized (this.singletonObjects) {  // 关键:同步锁
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null) {
      singletonObject = singletonFactory.getObject();
      this.singletonObjects.put(beanName, singletonObject);
    }
    return singletonObject;
  }
}

但是 Bean 创建完成后的使用过程,Spring 就不管了

换句话说,单例 Bean 在被创建后,如果它的内部状态是可变的,那么在多线程环境下就可能会出现线程安全问题

如果 Bean 中没有成员变量,或者成员变量都是不可变的,final 修饰的,那么就不存在线程安全问题

@Service
public class UserServiceImpl implements UserService {
  @Resource
  private UserDao userDao;
  @Autowired
  private CountService countService;
  // 只有依赖注入的无状态字段
}

@Service
public class ConfigService {
  private final String appName;  // final修饰,不可变
  
  public ConfigService(@Value("${app.name}") String appName) {
      this.appName = appName;
  }
}

如何解决单例 Bean 的线程安全问题

无状态优先,ThreadLocal 隔离,JUC 工具,加锁保底,改作用域兜后

方案适用场景核心思想
无状态设计通用不存状态,数据走方法参数
ThreadLocal需要线程独立状态每个线程一份副本
线程安全类计数/缓存用 JUC(AtomicInteger、ConcurrentHashMap)
加锁复杂状态操作synchronized / Lock
改作用域必须有状态prototype / request 每次新建

第一种,使用局部变量,也就是使用无状态的单例 Bean,把所有状态都通过方法参数传递

@Service
public class UserService {
  @Autowired
  private UserDao userDao;
  
  // 无状态方法,所有数据通过参数传递
  public User processUser(Long userId, String operation) {
    User user = userDao.findById(userId);
    // 处理逻辑...
    return user;
  }
}

第二种,当确实需要维护线程相关的状态时,可以使用 ThreadLocal 来保存状态。ThreadLocal 可以保证每个线程都有自己的变量副本,互不干扰

@Service
public class UserContextService {
  private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
  
  public void setCurrentUser(User user) {
    userThreadLocal.set(user);
  }
  
  public User getCurrentUser() {
    return userThreadLocal.get();
  }
  
  public void clear() {
    userThreadLocal.remove(); // 防止内存泄漏
  }
}

第三种,如果需要缓存数据或者计数,使用 JUC 包下的线程安全类,比如说 AtomicInteger、ConcurrentHashMap、CopyOnWriteArrayList 等

第四种,对于复杂的状态操作,可以使用 synchronized 或 Lock

第五种,如果 Bean 确实需要维护状态,可以考虑将其改为 prototype 作用域,这样每次注入都会创建一个新的实例,避免了多线程共享同一个实例的问题,或者使用 request 作用域,这样每个 HTTP 请求都会创建一个新的实例

@Resource@Autowired 区别

当使用 @Autowired 注解注入 Bean 时,IDEA 会提示“Field injection is not recommended”

@Autowired 是 Spring 框架提供的注解,而 @Resource 是 Java EE 标准提供的注解

换句话说,@Resource 是 JDK 自带的,而 @Autowired 是 Spring 特有的

区别

从注入方式上说,@Autowired 默认按照类型,也就是 byType 进行注入,而 @Resource 默认按照名称,也就是 byName 进行注入

当容器中存在多个相同类型的 Bean, 比如说有两个 UserRepository 的实现类,直接用 @Autowired 注入 UserRepository 时就会报错,因为 Spring 容器不知道该注入哪个实现类

@Component
public class UserRepository21 implements UserRepository2 {}

@Component
public class UserRepository22 implements UserRepository2 {}

@Component
public class UserService2 {
  @Autowired
  private UserRepository2 userRepository; // 冲突
}
上次编辑于: