当前位置: 首页 > news >正文

浅谈SpringBoot框架中的单例bean

 思维导图:

  在 Spring Boot 里,默认情况下 Bean 是单例的。单例 Bean 意味着在整个应用程序上下文里只有一个实例。要是这个单例 Bean 有状态(也就是包含实例变量),而且多个线程同时对这些实例变量进行读写操作,就可能产生线程安全问题。下面是一个具体的示例。


1. 创建 Spring Boot 项目

可以使用 Spring Initializr(https://start.spring.io/)创建一个包含 Spring Web 依赖的 Spring Boot 项目。

2. 定义有状态的单例 Bean

创建一个 CounterService 类,它有一个实例变量 count,还有两个方法分别用于增加计数和获取计数。

package com.example.demo.service;import org.springframework.stereotype.Service;@Service
public class CounterService {private int count = 0;public void increment() {try {// 模拟耗时操作,放大线程安全问题Thread.sleep(1);} catch (InterruptedException e) {Thread.currentThread().interrupt();}count++; // 非原子操作,多线程下易出现问题}public int getCount() {return count;}
}

 3. 创建控制器来调用 Bean

创建一个 CounterController 类,它会注入 CounterService中的 Bean,并且提供两个端点,一个用于增加计数,另一个用于获取计数。

package com.example.demo.controller;import com.example.demo.service.CounterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/user")
public class CounterController {@Autowiredprivate CounterService counterService;@GetMapping("/increment")public String increment() {counterService.increment();return "Count incremented";}@GetMapping("/count")public int getCount() {return counterService.getCount();}
}

4. 测试线程安全问题

可以使用多线程工具(如 Apache JMeter 或者编写一个简单的 Java 多线程程序)来模拟多个线程同时访问 /increment 端点。以下是一个简单的 Java 多线程测试代码:

package com.example.demo;import com.example.demo.service.CounterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;@SpringBootApplication
public class DemoApplication implements CommandLineRunner {@Autowiredprivate CounterService counterService;public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}@Overridepublic void run(String... args) throws Exception {ExecutorService executorService = Executors.newFixedThreadPool(50); // 50 个线程并发int threadCount = 1000;for (int i = 0; i < threadCount; i++) {executorService.submit(() -> counterService.increment());}executorService.shutdown();while (!executorService.isTerminated()) {// 等待所有任务完成}System.out.println("Expected count: " + threadCount); // 预期 1000System.out.println("Actual count: " + counterService.getCount()); // 实际值很可能小于 1000}
}

原理分析

count++ 操作由 “读取 count 值 → 对值加 1 → 写回新值” 三个步骤组成,并非原子操作。
当多个线程执行到 Thread.sleep(1) 时,会释放 CPU 时间片,其他线程可能在此时读取到相同的 count 值,导致最终结果小于预期(如 998、990 等),从而体现出单例 Bean(有状态且无同步措施)的线程安全问题。

 

解决方案

在 Spring Boot 中,若单例 Bean 存在状态(如包含可变成员变量),需通过以下方式解决线程安全问题:

1. 同步机制(如 synchronized

通过对方法或代码块加锁,确保同一时间只有一个线程执行关键代码。

import org.springframework.stereotype.Service;@Service
public class CounterService {private int count = 0;// 对方法加锁,保证原子性public synchronized void increment() {count++;}public int getCount() {return count;}
}

原理:synchronized 会在方法或代码块执行时获取锁,其他线程需等待锁释放,避免多个线程同时修改共享变量。

2. 使用原子类(如 AtomicInteger

Java 并发包 java.util.concurrent.atomic 提供的原子类,其方法(如 incrementAndGet)内部通过底层机制(如 CAS)保证操作原子性。

import org.springframework.stereotype.Service;
import java.util.concurrent.atomic.AtomicInteger;@Service
public class CounterService {private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet(); // 原子操作}public int getCount() {return count.get();}
}

 原理:原子类通过硬件级别的原子操作或乐观锁(CAS)确保对变量的修改是线程安全的,避免传统锁的性能开销。

3. ThreadLocal 隔离线程变量

为每个线程提供独立的变量副本,避免线程间共享数据。

import org.springframework.stereotype.Service;@Service
public class CounterService {private ThreadLocal<Integer> count = ThreadLocal.withInitial(() -> 0);public void increment() {count.set(count.get() + 1); // 操作当前线程的副本}public int getCount() {return count.get();}// 建议在使用后清理,防止内存泄漏public void clean() {count.remove();}
}

4. 修改 Bean 作用域为 prototype

将单例(singleton)改为原型(prototype),每次请求都会创建新的 Bean 实例,避免多线程共享状态。

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;@Service
@Scope("prototype") // 每次请求创建新实例
public class CounterService {private int count = 0;public void increment() {count++;}public int getCount() {return count;}
}

原理:原型作用域下,每个线程获取独立的 Bean 实例,线程间无共享状态,从而避免线程安全问题,但会增加对象创建开销。

  实际应用中,可根据场景选择:

  • 简单场景优先用 原子类(性能高)或 同步方法(实现简单)。
  • 若变量需线程内共享但跨线程隔离,用 ThreadLocal
  • 若 Bean 实例创建开销小且需彻底隔离,用 prototype 作用域

相关文章:

  • 【KWDB 创作者计划】利用KWDB解决工业物联网场景中的海量数据管理难题的思考
  • 代码随想录单调栈part2
  • C 语言 第五章 指针(6)
  • 网工_IP协议
  • 【算法基础】冒泡排序算法 - JAVA
  • 【质量管理】现代TRIZ问题识别中的功能分析——组件分析
  • 从零开始讲DDR(9)——AXI 接口MIG 使用(2)
  • 文章五《卷积神经网络(CNN)与图像处理》
  • 第 6 篇:AVL 树与 SB 树:不同维度的平衡探索 (对比项)
  • 【质量管理】现代TRIZ问题识别中的功能分析——相互接触分析
  • 滑动窗口leetcode 209和76
  • PostgreSQL自定义函数
  • 特殊运算符详解:身份运算符、成员运算符与三目运算符
  • Ghost-Downloader-3开源下载软件,使用 Python 构建的跨平台 Fluent-Design AI-boost 多线程下载器
  • 使用mybatis实例类和MySQL表的字段不一致怎么办
  • 文件操作--文件下载读取漏洞
  • Fedora升级Google Chrome出现GPG check FAILED问题解决办法
  • 【Linux】关于虚拟机
  • 【AI提示词】奥卡姆剃刀思维模型专家
  • Semantic Kernel 快速入门
  • 因雷雨、沙尘等天气,这些机场航班运行可能受影响
  • 菏泽家长“付费查成绩”风波调查:免费功能被误读
  • 海港负国安主场两连败,五强争冠卫冕冠军开始掉队
  • 万达电影去年净利润亏损约9.4亿元,计划未来三年内新增25块IMAX银幕
  • 擦亮“世界美食之都”金字招牌,淮安的努力不止于餐桌
  • 光明日报:回应辅警“转正”呼声,是一门政民互动公开课