核心概念
Spring Bean本身不是线程安全的。Spring容器只负责Bean的生命周期管理和依赖注入,不提供线程安全保障。
Bean的线程安全性取决于:
- Bean的作用域:singleton、prototype等
- Bean是否有可变状态:成员变量
- Bean的使用方式:是否在多线程环境下共享
简单来说:
- 无状态Bean(没有成员变量或只有final成员变量)→ 线程安全
- 有状态Bean(有可变成员变量)→ 需要额外处理才能保证线程安全
不同作用域下的线程安全性
1. singleton(单例)- 需要注意线程安全
特点:
- 整个应用共享一个实例
- 多线程并发访问同一个对象
- 默认不是线程安全的
// ❌ 线程不安全的单例Bean
@Service
public class UnsafeCounterService {
private int count = 0; // 共享可变状态
public void increment() {
count++; // 非原子操作,线程不安全
}
public int getCount() {
return count;
}
}
// 测试:多线程并发调用
@SpringBootTest
public class ThreadSafetyTest {
@Autowired
private UnsafeCounterService counterService;
@Test
public void testConcurrency() throws InterruptedException {
int threadCount = 100;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
counterService.increment();
}
latch.countDown();
}).start();
}
latch.await();
// 预期:10000,实际:可能小于10000(线程不安全)
System.out.println("Count: " + counterService.getCount());
}
}
2. prototype(原型)- 天然线程安全
特点:
- 每次获取都是新实例
- 不同线程使用不同对象
- 天然线程安全(但要注意对象本身的线程安全性)
@Service
@Scope("prototype")
public class PrototypeService {
private int count = 0; // 每个实例独立
public void increment() {
count++; // 线程安全(不同线程使用不同实例)
}
public int getCount() {
return count;
}
}
注意:原型Bean虽然线程安全,但有性能开销(频繁创建对象)。
3. request/session/application - 根据隔离级别决定
- request作用域:每个HTTP请求独立,请求内线程安全
- session作用域:每个会话独立,会话内线程安全
- application作用域:全局共享,与singleton相同的线程安全问题
线程安全解决方案
方案1:无状态设计(推荐)
最佳实践是设计无状态的Bean,不使用可变成员变量。
// ✅ 线程安全的无状态Bean
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // 注入的依赖(不可变)
// 没有可变成员变量,所有数据通过参数传递
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
public void updateUser(User user) {
userRepository.save(user);
}
}
方案2:使用ThreadLocal
每个线程维护自己的变量副本,实现线程隔离。
@Service
public class ThreadLocalService {
private static final ThreadLocal<User> CURRENT_USER = new ThreadLocal<>();
public void setCurrentUser(User user) {
CURRENT_USER.set(user);
}
public User getCurrentUser() {
return CURRENT_USER.get();
}
public void clear() {
CURRENT_USER.remove(); // 务必清理,避免内存泄漏
}
}
// 使用拦截器自动清理
@Component
public class UserContextInterceptor implements HandlerInterceptor {
@Autowired
private ThreadLocalService threadLocalService;
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
threadLocalService.clear(); // 请求结束后清理
}
}
ThreadLocal最佳实践:
- 使用
static final修饰 - 使用完毕后调用
remove()清理 - 避免在线程池场景下内存泄漏
方案3:使用同步机制
3.1 synchronized关键字
@Service
public class SynchronizedService {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
优点:简单易用
缺点:性能较差,锁粒度大
3.2 Lock锁
@Service
public class LockService {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
优点:更灵活(可中断、可超时)
缺点:代码复杂度高
3.3 原子类
@Service
public class AtomicService {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
优点:性能好,无锁算法(CAS)
缺点:只适用于简单类型
方案4:不可变对象
@Service
public class ImmutableService {
private final String serviceName;
private final int maxRetry;
// 所有字段都是final,构造后不可变
public ImmutableService(@Value("${service.name}") String serviceName,
@Value("${service.maxRetry}") int maxRetry) {
this.serviceName = serviceName;
this.maxRetry = maxRetry;
}
// 只提供读取方法,不提供修改方法
public String getServiceName() {
return serviceName;
}
public int getMaxRetry() {
return maxRetry;
}
}
方案5:使用并发集合
@Service
public class CacheService {
// 使用线程安全的集合
private final ConcurrentHashMap<String, User> userCache = new ConcurrentHashMap<>();
private final CopyOnWriteArrayList<String> recentSearches = new CopyOnWriteArrayList<>();
public void cacheUser(String id, User user) {
userCache.put(id, user);
}
public User getUser(String id) {
return userCache.get(id);
}
public void addSearch(String keyword) {
recentSearches.add(keyword);
}
}
实战案例
案例1:Controller中的线程安全
// ❌ 错误:Controller中使用可变成员变量
@RestController
@RequestMapping("/api/users")
public class UnsafeUserController {
private User currentUser; // 多个请求共享,线程不安全
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
currentUser = userService.findById(id);
return currentUser;
}
}
// ✅ 正确:不使用成员变量
@RestController
@RequestMapping("/api/users")
public class SafeUserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // 通过参数传递
}
}
// ✅ 正确:使用ThreadLocal或request作用域
@RestController
@RequestMapping("/api/users")
public class SafeUserController2 {
@Autowired
private ThreadLocalService threadLocalService;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
User user = userService.findById(id);
threadLocalService.setCurrentUser(user);
return user;
}
}
案例2:Service中的线程安全
// ❌ 错误:Service中缓存可变对象
@Service
public class UnsafeOrderService {
private List<Order> cachedOrders = new ArrayList<>(); // 线程不安全
public void addOrder(Order order) {
cachedOrders.add(order); // 并发修改异常
}
}
// ✅ 正确:使用线程安全的集合
@Service
public class SafeOrderService {
private final ConcurrentHashMap<Long, Order> orderCache = new ConcurrentHashMap<>();
public void addOrder(Order order) {
orderCache.put(order.getId(), order);
}
public Order getOrder(Long id) {
return orderCache.get(id);
}
}
// ✅ 更好:使用专业的缓存框架
@Service
public class BetterOrderService {
@Autowired
private CacheManager cacheManager;
@Cacheable(value = "orders", key = "#id")
public Order getOrder(Long id) {
return orderRepository.findById(id).orElse(null);
}
}
案例3:DateFormat线程安全问题
// ❌ 错误:SimpleDateFormat不是线程安全的
@Service
public class UnsafeDateService {
private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
public String format(Date date) {
return sdf.format(date); // 线程不安全
}
}
// ✅ 方案1:使用ThreadLocal
@Service
public class SafeDateService1 {
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public String format(Date date) {
return DATE_FORMAT.get().format(date);
}
}
// ✅ 方案2:使用DateTimeFormatter(推荐)
@Service
public class SafeDateService2 {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd"); // 线程安全
public String format(LocalDate date) {
return date.format(FORMATTER);
}
}
// ✅ 方案3:每次创建新实例
@Service
public class SafeDateService3 {
public String format(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(date);
}
}
常见陷阱与最佳实践
陷阱1:注入的Bean也是单例
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB; // serviceB也是单例
// 如果ServiceB不是线程安全的,这里也会有问题
}
陷阱2:静态变量
@Service
public class StaticFieldService {
private static int count = 0; // 静态变量在所有实例间共享
public void increment() {
count++; // 线程不安全
}
}
最佳实践
- 优先设计无状态Bean
- 避免在单例Bean中使用可变成员变量
- 使用ThreadLocal时务必清理
- 对于必须有状态的Bean,使用prototype作用域
- 使用并发工具类(Atomic、ConcurrentHashMap等)
- 注意第三方库的线程安全性(如SimpleDateFormat)
性能对比
// 性能测试
@SpringBootTest
public class PerformanceTest {
@Test
public void comparePerformance() throws Exception {
int iterations = 1000000;
// 1. 无锁(最快)
AtomicInteger atomic = new AtomicInteger(0);
long start1 = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
atomic.incrementAndGet();
}
System.out.println("Atomic: " + (System.currentTimeMillis() - start1) + "ms");
// 2. synchronized(较慢)
int[] count = {0};
long start2 = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
synchronized (count) {
count[0]++;
}
}
System.out.println("Synchronized: " + (System.currentTimeMillis() - start2) + "ms");
// 3. ReentrantLock(较慢)
ReentrantLock lock = new ReentrantLock();
int[] count2 = {0};
long start3 = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
lock.lock();
try {
count2[0]++;
} finally {
lock.unlock();
}
}
System.out.println("ReentrantLock: " + (System.currentTimeMillis() - start3) + "ms");
}
}
性能排序:无锁 > Atomic > synchronized > Lock
面试总结
核心要点
- Spring Bean默认不是线程安全的,需要开发者自行保证
- 单例Bean是最常见的线程安全问题来源
- 无状态Bean天然线程安全,推荐设计
- 原型Bean每次创建新实例,避免共享状态
- 解决方案:无状态设计、ThreadLocal、同步机制、原子类、并发集合
常见面试问题
Q1: Spring的单例Bean在多线程环境下安全吗?
A: 不一定。如果是无状态Bean(没有可变成员变量),则线程安全;如果有可变状态,需要额外处理。
Q2: 如何保证单例Bean的线程安全?
A: ① 设计为无状态Bean;② 使用ThreadLocal;③ 使用同步机制;④ 使用并发工具类;⑤ 改为prototype作用域。
Q3: Controller是线程安全的吗?
A: Controller默认是单例,如果不使用成员变量则线程安全;如果使用了成员变量,则不安全。
Q4: SimpleDateFormat为什么不能作为成员变量?
A: SimpleDateFormat不是线程安全的,在单例Bean中使用会导致并发问题,应使用ThreadLocal或DateTimeFormatter。
Q5: prototype作用域能解决所有线程安全问题吗?
A: 不能。prototype只是每次创建新实例,但如果对象本身不是线程安全的(如使用了非线程安全的集合),仍然有问题。