96SEO 2026-08-15 09:18 40
凌晨 3 点,我被
登上机器看了下 GC 日志:
Full GC
堆几乎占满了Full GC 回收不动。2G 的堆,0.8G 赖着不走。
用 jmap -histo:live 看了一下排第一的是:
num #instances #bytes class name的 154MB 中。粗略估算,这个UserContext对象本身约占 88MB。其间接引用的 byte 也包含在 154MB 的中,合计占用远超 154MB,而且还在涨。按理说,我们整个程序只有 1 个并发使用者。怎么会有 10000 个 UserContext?
排查之后发现这方面,问题出在 ThreadLocal 上。说得更准确点,出在线程池 + ThreadLocal 的组合上。
二、背景:我们怎么用 ThreadLocal 的
我们的订单服务有一个
UserContext保存当前登录使用者的信息,在 Controller 层设置。在 Service 层读取:public class UserContext { private static final ThreadLocalCONTEXT = new ThreadLocal<>;private Long userId;说起来,private String username;private List permissions;// 权限列表,比较大 public static void set { CONTEXT.set;} public static UserContext get { return CONTEXT.get;} public static void clear { CONTEXT.remove;} } 在拦截器里设置和清理:
@Component public class UserContextInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request。HttpServletResponse response,Object handler) { // 从 token 解析使用者信息,设置到 ThreadLocal UserContext ctx = parseToken);UserContext.set;老实说,return true;} @Override public void afterCompletion(HttpServletRequest request。HttpServletResponse response,Object handler,Exception ex) { UserContext.clear;// 请求结束,清理 ThreadLocal } }这套用法看起来没问题。Tomcat 的线程处理完请求后
afterCompletion问题出在异步任务上。
三、真正的麻烦:线程池里的"脏数据"
问题 :线程池复用 → ThreadLocal 脏数据
我们的订单服务有异步处理逻辑:
@Service public class OrderService { @Autowired private ThreadPoolTaskExecutor executor;public void createOrder { // 主线程里 UserContext 有值 UserContext ctx = UserContext.get;// ✅ 正常 // 把上下文传给异步任务 UserContext finalCtx = ctx;executor.submit -> { UserContext.set;// 手动设置到子线程 doAsyncWork;// ⚠️ 如果这里忘了调 UserContext.clear // 这个线程执行完任务后ThreadLocalMap 里还留着 finalCtx });} }标准
ThreadLocal不会把值自动传给子线程,所以很多人会像上面这样手动set。但set后忘记removeThreadLocalMap 的数据就留下来了。"脏数据"案例:
// 线程-1 先后执行了三个任务 // 任务A:使用者张三的请求 → UserContext.set → 没有 remove // 任务B:使用者李四的请求 → UserContext.get → 拿到的是张三的数据!其实,// 任务C:使用者王五的请求 → UserContext.get → 还是张三的数据!
I nheritableThreadLocal,父线程的值会"继承"给子线程。线程池复用后这个值跟着传递,造成跨使用者的上下文混乱。
安全危机
问题 :线程池 + 不 remove → 内存泄漏
This is root cause of OOM.
Per‑thread storage is a custom hash map called
Thread 对象 └── ThreadLo calMap └── Entry └── Entry extends WeakReference ├── key: ThreadLo cal 对象→可能被 GC 回收 └── value: 您存入的对象→不会被 GC 回收 泄漏的两条方法
方法① :Key 被 GC 方法② :线 程 池 中线 程不销毁
方法① 示例 : public void process { ThreadLoca l tl = new Thre adLo cal<>;// 局部变量 tl.set);// 方法结束后 tl 出栈,没有强引用 // 此时 Entry.key 为弱引用。被 GC 回收 // key 为 null,但 value 仍然存在!}
⚠️ 实际项目中几乎不会出现这种情况,因为大多数项目把 ThreadL ocal 定义为 `private static final`。真正导致泄漏的是 **方法②** —— 在线程池里长期复用同一条线 程而没有显式 remove。 JDK 的“自愈”机制
前提一:必须调用 get/set/remove 前提二:如果很久没有 访问同一个 TL,则自愈不会及时触发。
:JDK 自己做一次“兜底”,但绝不能当作保证。正确做法永远是 **主动 remove**。五、方法
再看方案一,try‑finally 确保清理
executor.submit -> { try { UserCon tex t.set;不过,// 设置 doAsyncWork;} finally { Us erCon tex t.clear;// 必须清理,} });This is baseline. 至于方案二,自定义 ThreadPoolTaskExecutor 在层面统一清理
If you sprinkle try‑finally everywhere。someone will inevitably forget.
/@Component public class T hreadL ocalAwareTaskExecutor extends T hreadPoo lTaskExecutor { @Override public void execute { // Capture snapshot of submit‑thread's TLs Map snapshot = captureThreadLocals;super.execute -> { try { restoreThreadLocals;话说回来,command.run;} finally { cleanAllThreadLocals;} }),} private MapcaptureT hreadLocals { // Use TTL's snapshot ability or maintain your own registry Map snap = new HashMap<>;// ,populate snap…return snap,} private void restoreT hreadLocals { snap.forEach -> { if tl.set;else tl.remove;}),} private void cleanAllT hreadLocals { UserCon tex t.clear;其实,TraceCo ntext.clear;// ,or TLs…} }
方案三这方面,使用 TransmittableThreadLocal
The open‑source library from Alibaba does exactly what you need.
/ / 把 ThreadLo cal 换成 TransmittableThre adLo cal public class U serCo ntext { private static final TransmittableThrea dLocal public static void set{ CON TEX T.se t;} public static Us erCon tex t get{ retu rn CONT EXT.g et;} public static vo id clear{ CON TEX T.re mo ve;说起来,} }
/ / Option A: Java Agent java -javaagent:transmittable-thread-local‑x.jar ... // Option B: manual wrapper ExecutorService raw = Executors.newFixedT hreadPool;ExecutorService exec = TtlExecutors.getTtlExecutorService;
// Spring Boot bean example @Bean public T hreadPo olTaskExecu tor taskExecutor { T hreadPo olTaskExecu tor exec = new T hreadPo olTaskExecu tor;exec.setCorePoolSize;exec.setMaxPoolSize;exec.setTa skDecorator;// ★关键★ TTL 包装 exec.initialize;return exec,怎么说呢,}
自动传递:: 提交时复制父线程 TL 值到工作线 程。 执行后恢复快照:: 完成后恢复工作线 程之前的状态。即使它之前已有值,也能保持干净。 不干扰已有值:: 若工作线 程已有 TL,它们会被暂时保存并在任务结束后恢复。
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback