文章目录
1、作用: ThreadLocal提供线程局部变量,这种变量在每个线程中都是独立的,一个线程对ThreadLocal变量的修改不会影响其他线程。
2、使用场景: 常用于实现线程安全,尤其是在多线程环境中对于那些需要避免共享的变量。例如,在Web应用中存储用户的会话信息、数据库连接等。
凌晨 3 点被运维喊醒后,我靠 ThreadLocal 救了支付系统

零、引子
“哥,快起来!支付系统炸了!” 凌晨三点的手机铃声比我家猫踩键盘还刺耳,运维老王的大嗓门快把我耳膜震破,“用户 A 充 100 块,余额变成用户 B 的了,现在客服电话被打爆了!”
我猛地坐起来,睡衣上还沾着昨晚调试代码的咖啡渍 —— 作为公司 “背锅侠” 兼 Java 开发,这种凌晨排障的戏码比女朋友的脾气还难预测。抓起笔记本电脑冲进书房,远程连进生产环境日志,一行红色报错旁边的代码让我差点把刚喝的可乐喷在屏幕上:
// 同事写的“共享用户信息”代码
private static User currentUser;
public void handlePayment(User user, BigDecimal money) {
currentUser = user; // 存用户信息
checkBalance(); // 查余额
deductMoney(money); // 扣钱
sendNotify(); // 发通知
}
“王德发, 这兄弟怕不是把多线程当单机游戏玩?” 我拍了下大腿。静态变量currentUser就像公司茶水间的公用马克杯,谁都拿来用,线程 A 刚把用户 A 的信息存进去,线程 B 立马把用户 B 的信息盖上去,查余额的时候能不错乱才怪 —— 这就像你刚把咖啡倒进杯子,同事反手给你换成了酱油,喝的时候那叫一个 “提神醒脑”。
一、救场英雄:ThreadLocal 登场
1.1 闪亮登场

眼看用户投诉量快赶上双十一订单了,我脑子里闪过一个关键词:ThreadLocal。这东西说白了,就是给每个线程配个 “私人储物柜”,线程 A 的东西放 A 的柜子,线程 B 的放 B 的柜子,互不干扰,再也不用抢公用马克杯。
我花 5 分钟改了代码,把静态变量换成 ThreadLocal:
// 线程私有,谁用谁拿自己的
private static ThreadLocal<User> currentUser = ThreadLocal.withInitial(() -> null);
public void handlePayment(User user, BigDecimal money) {
try {
currentUser.set(user); // 给当前线程存用户
checkBalance(); // 查的是当前线程的用户余额
deductMoney(money);
sendNotify();
} finally {
currentUser.remove(); // 用完清空,免得占地方
}
}
// 查余额的时候直接取,不用传参数
private void checkBalance() {
User user = currentUser.get(); // 每个线程拿到自己的用户
if (user.getBalance().compareTo(BigDecimal.ZERO) < 0) {
throw new InsufficientBalanceException("余额不足,别浪了");
}
}
重启服务,监控面板上的错误率瞬间从 99% 跌到 0,老王发来一句 “哥,牛批!”,我终于能瘫回椅子上 —— 这 ThreadLocal,简直是多线程乱局里的 “秩序维护官”。
1.2 ThreadLocal:每个线程的 “私人秘书”

很多新手觉得 ThreadLocal 高深,其实你把线程想象成公司的员工,ThreadLocal 就是每个员工的专属秘书:
- 秘书只服务一个老板:ThreadLocal 为每个线程创建独立的数据副本,线程 A 的set()操作,只有 A 的get()能拿到,线程 B 就算喊破嗓子也拿不到 A 的数据。就像我的秘书不会把我的日程表给隔壁开发看。
- 不用到处递文件:以前传递用户信息,得从handlePayment()传到checkBalance(),再传到sendNotify(),方法参数堆得像小山。现在用 ThreadLocal 存一次,整个线程里的方法随时能取,这效率堪比秘书直接把文件送到你桌上。
- 下班前要清桌面:用完必须调用remove(),不然线程池里的线程复用会导致 “旧数据残留”—— 这就像秘书下班不收拾桌面,第二天新老板来上班,看到的还是前老板的文件,不乱才怪。
1.3 这些场景,ThreadLocal 是 “刚需”
ThreadLocal 不是万能的,但在这些场景里,它比 synchronized好用 10 倍,还不用加锁等锁:
-
✔️Web 开发:用户上下文传递
- 你在浏览器登录后,每次请求都是一个线程。
- 用 ThreadLocal 存登录用户的Token、权限信息,Controller、Service、Dao 层不用层层传参数,直接get()就能用。就像你进公司刷一次工卡,全公司的系统都知道你是谁,不用每个部门都出示一次证件。
- Spring 的RequestContextHolder就是这么实现的,底层藏着一个ThreadLocal,帮你把请求信息存得明明白白。
-
➡️ 数据库连接:避免线程争抢
- 数据库连接Connection是线程不安全的,以前用连接池的时候,得手动拿连接、用完放回去。
- 现在用 ThreadLocal 绑定连接,每个线程拿自己的连接,用完在finally里释放,再也不用担心线程 A 把线程 B 的连接关了 —— 这就像每个员工都有自己的办公电脑,不用抢着用一台。
-
💯线程级缓存:临时数据存起来
- 比如解析一个超大 JSON,里面的配置信息线程内要用到多次,存到 ThreadLocal 里,不用每次都解析一遍。
- 这就像你把常用的 API 文档存到桌面,比每次都去网盘下载快多了。
1.4 踩过的坑:ThreadLocal 的 “暗雷”

别以为 ThreadLocal 是万能药,我当年刚用的时候,就踩过一个 “内存泄漏” 的大坑:
有次用线程池处理任务,ThreadLocal 里存了很大的Excel数据,用完没remove()。结果线程池里的线程反复复用,数据越积越多,JVM 内存直接飙到 90%,差点触发熔断。
后来才知道,ThreadLocal 的底层是Thread类里的ThreadLocalMap,key 是 ThreadLocal 实例,value 是数据。如果不用remove(),线程池的线程不销毁,这个 key-value 就一直占着内存 —— 这就像你离职了,私人物品还堆在公司工位,占着地方不让新人用。
所以记住:用 ThreadLocal 一定要写在 try-finally 里,finally 里必须 remove () ,这是程序员的职业素养,就像上完厕所要冲水一样天经地义。
1.5 🥇🥇🥇🥇🥇其它方案

其实ThreadLocal有很多问题,可以采用如下方案进行替换:
- InheritableThreadLocal
- ScopedValue (Java19+)
- TransmittableThreadLocal(TTL,阿里开源)
- FastThreadLocal(Netty common提供)
- NamedThreadLocal(Spring框架)
详情了解可查看文档: https://mp.weixin.qq.com/s/9zoG0O5aTwgaBNC7WMZmpA
文档
二、总结
讲了这么多,ThreadLocal 的核心就一句话:通过线程私有数据副本,实现多线程数据隔离。它不解决线程同步问题,而是从根源上避免了同步 —— 既然大家不用抢同一个东西,自然就不会打架。
最后送两个彩蛋:
- 初始化技巧:Java 8 + 可以用ThreadLocal.withInitial(()->new User())初始化,比set()更优雅,就像秘书提前给你准备好办公用品。
- 面试必问:ThreadLocal 和 synchronized 的区别?答案是:synchronized 是 “让大家排队用”,ThreadLocal 是 “给每个人发一个”,前者解决竞争,后者避免竞争。
现在凌晨五点,天已经蒙蒙亮了,支付系统恢复正常,我的咖啡也喝完了。
如果你觉得这个故事有用,点赞 + 分享给你那写静态变量的同事,免得他下次再坑你。关注我,下次讲线程池的那些 “坑”,保证让你在面试和工作中少走弯路 —— 毕竟,程序员的快乐,就是少踩坑、早下班、多陪猫。


哦, 王德发, 放错图了 ~~~ 是这个猫

完结~~~
ThreadLocal详解及应用场景
10万+

被折叠的 条评论
为什么被折叠?



