浅谈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
作用域。