完整教程:在 SpringBoot+Tomcat 环境中 线程安全问题的根本原因以及哪些变量会存在线程安全的问题

前言

本文带你去深入理解为什么在web环境中(Tomcat +SpringBoot)会存在多线程的问题以及哪些变量会存在线程安全的问题。

Tomcat + SpringBoot

首先我们来看下Tomcat的多线程处理模型

1、Tomcat内部维护一个工作线程池
2、每个HTTP请求由Tomcat线程池中的一个工作线程处理
3、在高并发场景下,多个线程同时处理不同的HTTP请求

Spring Boot是如何去加载类的:

1、@Component 等注解修饰的类类会被 Spring 扫描到,并放入容器中成为 Bean
2、Spring容器中的Bean是单例的
3、所有请求共享同一个单例的Bean 类
4、所有线程获得的是同一个Bean 类的引用

正是由于Bean是单例的+每个HTTP请求一个工作线程处理
所以存在多个工作线程同时操作一个Bean实例,这样就导致了多线程竞争同一个资源,进而导致线程安全的问题。

实际例子去理解单例和多例加载:

单例加载

@Component
public
class SingletonCounterService {
private
int count = 0
;
public
void increase(
) {
count++
;
System.out.println(Thread.currentThread(
).getName(
) + " count = " + count)
;
}
}
@SpringBootTest
public
class SingletonTest {
@Autowired
private SingletonCounterService counter;
@Test
public
void testMultiThreadSingleton(
)
throws InterruptedException {
Runnable task = (
) -> counter.increase(
)
;
Thread t1 =
new Thread(task, "T1"
)
;
Thread t2 =
new Thread(task, "T2"
)
;
Thread t3 =
new Thread(task, "T3"
)
;
t1.start(
)
;
t2.start(
)
;
t3.start(
)
;
t1.join(
)
;
t2.join(
)
;
t3.join(
)
;
}
}
T1 count = 1
T2 count = 3
T3 count = 2

结果分析

多个线程同时操作同一个SingletonCounterService实例 内部的共享变量count 导致最后 count为3 而不是每一个都为1

多例加载:

@Component
@Scope
("prototype"
)
public
class PrototypeCounterService {
private
int count = 0
;
public
void increase(
) {
count++
;
System.out.println(Thread.currentThread(
).getName(
) + " count = " + count)
;
}
}
@SpringBootTest
public
class PrototypeTest {
@Autowired
private ApplicationContext context;
@Test
public
void testMultiThreadPrototype(
)
throws InterruptedException {
Runnable task = (
) ->
{
PrototypeCounterService counter = context.getBean(PrototypeCounterService.
class
)
;
counter.increase(
)
;
// 每个线程是自己独立的 bean
}
;
Thread t1 =
new Thread(task, "T1"
)
;
Thread t2 =
new Thread(task, "T2"
)
;
Thread t3 =
new Thread(task, "T3"
)
;
t1.start(
)
;
t2.start(
)
;
t3.start(
)
;
t1.join(
)
;
t2.join(
)
;
t3.join(
)
;
}
}
T1 count = 1
T2 count = 1
T3 count = 1

结果分析:

每个线程拿到的是自己独立的 bean 实例,不共享count。

哪些变量存在线程安全的问题?

经过上面的分析,你已经知道了为什么会存在多线程的问题了吧。(多个线工作线程去操作同一个类实例)那么下一步就是去定位可能存在多线程安全的变量位置。

线程不安全

1、实例变量(成员变量):

@Service
public
class UserService {
private User currentUser;
// 不安全:多个线程可能同时修改currentUser
private
int counter = 0
;
// 不安全:多线程递增count不是原子操作
}

2、非线程安全的实例变量

@Service
public
class ReportService {
private SimpleDateFormat dateFormat =
new SimpleDateFormat("yyyy-MM-dd"
)
;
// 不安全:SimpleDateFormat非线程安全
private Map<
String
, Object> dataCache =
new HashMap<
>(
)
;
// 不安全:HashMap非线程安全
private List<
User> userCache =
new ArrayList<
>(
)
;
// 不安全:ArrayList是线程不安全的容器 相反vector是线程安全的
}

3、静态变量:

@Service
public
class ConfigService {
private
static Map<
String
, String> globalConfig =
new HashMap<
>(
)
;
// 不安全:类变量本身就是跨所有实例共享 
}

线程安全

1、方法内的局部变量:

public
void process(
) {
int localCounter = 0
;
// 安全:每个方法都有独立的方法栈 局部变量不共享
List<
String> localList =
new ArrayList<
>(
)
;
// 安全:局部变量
}

2、不可变(Immutable)实例变量

private
final String apiUrl = "https://api.example.com"
;
// 安全:不可变
private
final List<
String> constants = Collections.unmodifiableList(Arrays.asList("A"
, "B"
)
)
;
// 安全:不可变集合

3、线程安全的实例变量:

private AtomicInteger counter =
new AtomicInteger(0
)
;
// 安全:原子操作
private ConcurrentHashMap<
String
, User> userMap =
new ConcurrentHashMap<
>(
)
;
// 安全:并发集合

4、ThreadLocal变量

private ThreadLocal<
User> currentUser =
new ThreadLocal<
>(
)
;
// 安全:线程隔离

总结

在Spring+Tomcat环境中,线程安全问题的根本原因是:

Tomcat使用线程池并发处理HTTP请求
Spring默认使用单例Bean
这导致多个线程并发访问同一个Service实例
当Service包含可变共享状态时,就会出现线程安全问题
posted @ 2025-07-20 10:32  yjbjingcha  阅读(25)  评论(0)    收藏  举报