在 SpringBoot+Tomcat 环境中 线程安全问题的根本原因以及哪些变量会存在线程安全的问题。
文章目录
- 前言
- Tomcat + SpringBoot
- 单例加载
- 结果分析
- 多例加载:
- 结果分析:
- 哪些变量存在线程安全的问题?
- 线程不安全
- 线程安全
- 总结
前言
本文带你去深入理解为什么在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 {@Autowiredprivate SingletonCounterService counter;@Testpublic 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 {@Autowiredprivate ApplicationContext context;@Testpublic 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; // 不安全:多个线程可能同时修改currentUserprivate 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包含可变共享状态时,就会出现线程安全问题