【010】Java中的ThreadLocal变量及其使用场景

ThreadLocal详解及应用场景

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 就是每个员工的专属秘书:

  1. 秘书只服务一个老板:ThreadLocal 为每个线程创建独立的数据副本,线程 A 的set()操作,只有 A 的get()能拿到,线程 B 就算喊破嗓子也拿不到 A 的数据。就像我的秘书不会把我的日程表给隔壁开发看。
  2. 不用到处递文件:以前传递用户信息,得从handlePayment()传到checkBalance(),再传到sendNotify(),方法参数堆得像小山。现在用 ThreadLocal 存一次,整个线程里的方法随时能取,这效率堪比秘书直接把文件送到你桌上。
  3. 下班前要清桌面:用完必须调用remove(),不然线程池里的线程复用会导致 “旧数据残留”—— 这就像秘书下班不收拾桌面,第二天新老板来上班,看到的还是前老板的文件,不乱才怪。

1.3 这些场景,ThreadLocal 是 “刚需”

ThreadLocal 不是万能的,但在这些场景里,它比 synchronized好用 10 倍,还不用加锁等锁:

  1. ✔️Web 开发:用户上下文传递

    • 你在浏览器登录后,每次请求都是一个线程。
    • 用 ThreadLocal 存登录用户的Token、权限信息,Controller、Service、Dao 层不用层层传参数,直接get()就能用。就像你进公司刷一次工卡,全公司的系统都知道你是谁,不用每个部门都出示一次证件。
    • Spring 的RequestContextHolder就是这么实现的,底层藏着一个ThreadLocal,帮你把请求信息存得明明白白。
  2. ➡️ 数据库连接:避免线程争抢

    • 数据库连接Connection是线程不安全的,以前用连接池的时候,得手动拿连接、用完放回去。
    • 现在用 ThreadLocal 绑定连接,每个线程拿自己的连接,用完在finally里释放,再也不用担心线程 A 把线程 B 的连接关了 —— 这就像每个员工都有自己的办公电脑,不用抢着用一台。
  3. 💯线程级缓存:临时数据存起来

    • 比如解析一个超大 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 是 “给每个人发一个”,前者解决竞争,后者避免竞争。

现在凌晨五点,天已经蒙蒙亮了,支付系统恢复正常,我的咖啡也喝完了。

如果你觉得这个故事有用,点赞 + 分享给你那写静态变量的同事,免得他下次再坑你。关注我,下次讲线程池的那些 “坑”,保证让你在面试和工作中少走弯路 —— 毕竟,程序员的快乐,就是少踩坑、早下班、多陪猫。

请添加图片描述
请添加图片描述
哦, 王德发, 放错图了 ~~~ 是这个猫
请添加图片描述

完结~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值