文章目錄
- 前言
- 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包含可變共享狀態時,就會出現線程安全問題