# 第08天:ThreadLocal深入理解 ## 📚 今日目标 1. 理解ThreadLocal的概念和作用 2. 理解为什么用ThreadLocal存储路由信息 3. 实现DBContextHolder类 4. 理解内存泄漏问题 --- ## 🎯 知识点1:什么是ThreadLocal? ### 生活中的例子 **银行保险箱**: - 每个人有自己的保险箱 - 互不干扰 - 只能自己存取 **ThreadLocal**: - 每个线程有自己的变量副本 - 线程间互不干扰 - 线程内共享 ### 为什么需要ThreadLocal? **问题场景**: ```java // 方法1:设置路由信息 public void method1() { setRouteInfo("db01", "user_01"); method2(); // 调用方法2 } // 方法2:需要使用路由信息 public void method2() { String db = getRouteInfo(); // 如何获取? } ``` **解决方案**:ThreadLocal - 在方法1中设置 - 在方法2中获取 - 不需要传递参数 --- ## 🎯 知识点2:ThreadLocal的使用 ### 基本用法 ```java // 创建ThreadLocal ThreadLocal threadLocal = new ThreadLocal<>(); // 设置值 threadLocal.set("value"); // 获取值 String value = threadLocal.get(); // 删除值 threadLocal.remove(); ``` ### 为什么用ThreadLocal存储路由信息? **原因**: 1. **线程隔离**:每个请求是独立的线程,互不干扰 2. **无需传参**:不需要在每个方法中传递路由信息 3. **自动清理**:请求结束后,线程销毁,ThreadLocal自动清理 --- ## 🛠️ 实践任务:实现DBContextHolder ### 步骤1:创建DBContextHolder类 在 `src/main/java/cn/bugstack/middleware/db/router/` 目录下创建 `DBContextHolder.java`: ```java package cn.bugstack.middleware.db.router; /** * 数据库路由上下文持有者 * * 使用ThreadLocal存储当前线程的路由信息 * * @author 小傅哥 */ public class DBContextHolder { private static final ThreadLocal dbKey = new ThreadLocal<>(); private static final ThreadLocal tbKey = new ThreadLocal<>(); /** * 设置数据库索引 * * @param dbKeyIdx 数据库索引,如:db01, db02 */ public static void setDBKey(String dbKeyIdx) { dbKey.set(dbKeyIdx); } /** * 获取数据库索引 * * @return 数据库索引 */ public static String getDBKey() { return dbKey.get(); } /** * 设置表索引 * * @param tbKeyIdx 表索引,如:01, 02 */ public static void setTBKey(String tbKeyIdx) { tbKey.set(tbKeyIdx); } /** * 获取表索引 * * @return 表索引 */ public static String getTBKey() { return tbKey.get(); } /** * 清理数据库索引 */ public static void clearDBKey() { dbKey.remove(); } /** * 清理表索引 */ public static void clearTBKey() { tbKey.remove(); } /** * 清理所有路由信息 */ public static void clear() { dbKey.remove(); tbKey.remove(); } } ``` ### 代码解释 1. **ThreadLocal存储**: - `dbKey`:存储数据库索引(如:db01) - `tbKey`:存储表索引(如:01) 2. **静态方法**: - 所有方法都是静态的,方便调用 - 不需要创建实例 3. **清理方法**: - 必须手动清理,避免内存泄漏 - 在finally块中调用 --- ## 🎓 知识点拓展 ### 拓展1:ThreadLocal内存泄漏问题 **问题场景**: ```java ThreadLocal threadLocal = new ThreadLocal<>(); threadLocal.set("value"); // 忘记调用 remove() // 线程结束后,ThreadLocal的Entry还在,造成内存泄漏 ``` **原因**: - ThreadLocal的Entry是WeakReference - 但Value是强引用 - 如果ThreadLocal对象被回收,但线程还在,Value无法回收 **解决方案**: ```java try { threadLocal.set("value"); // 使用 } finally { threadLocal.remove(); // 必须清理 } ``` ### 拓展2:ThreadLocal的实现原理 **ThreadLocalMap**: - 每个Thread都有一个ThreadLocalMap - ThreadLocalMap的key是ThreadLocal对象 - ThreadLocalMap的value是存储的值 **存储结构**: ``` Thread └── ThreadLocalMap ├── Entry(ThreadLocal1, value1) ├── Entry(ThreadLocal2, value2) └── ... ``` ### 拓展3:InheritableThreadLocal **问题**:子线程无法继承父线程的ThreadLocal值 **解决方案**:InheritableThreadLocal ```java InheritableThreadLocal threadLocal = new InheritableThreadLocal<>(); threadLocal.set("value"); new Thread(() -> { String value = threadLocal.get(); // 可以获取到父线程的值 }).start(); ``` **我们项目不需要**: - 每个请求是独立的线程 - 不需要在子线程中共享路由信息 --- ## ✅ 今日检查清单 - [ ] 理解了ThreadLocal的概念和作用 - [ ] 理解了为什么用ThreadLocal存储路由信息 - [ ] 实现了DBContextHolder类 - [ ] 理解了内存泄漏问题 - [ ] 理解了ThreadLocal的实现原理 - [ ] 完成了拓展阅读 --- ## 🎯 明日预告 明天我们将学习: - 动态数据源原理 - AbstractRoutingDataSource - 实现DynamicDataSource类 --- ## 💡 思考题 1. 为什么用ThreadLocal而不是全局变量? 2. ThreadLocal的内存泄漏问题如何避免? 3. 什么时候需要清理ThreadLocal? --- ## 📚 参考资源 - [ThreadLocal官方文档](https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadLocal.html) - [ThreadLocal原理分析](https://www.baeldung.com/java-threadlocal) - [内存泄漏问题](https://www.baeldung.com/java-threadlocal-memory-leak)