翻车:SimpleDateFormat 的日期错乱
多个线程共用一个 SimpleDateFormat 解析日期,输出偶尔错乱,甚至抛 NumberFormatException。原因:sdf 内部有共享的可变状态(Calendar),并发使用互相污染。修法三条:每次 new(浪费)、调用加锁(串行化)、ThreadLocal——每个线程发一个自己的 sdf,互不干扰。第三条是标准答案。
结构:数据存在 Thread 身上,不在 ThreadLocal 里
直觉以为 ThreadLocal 是个存值的容器,错。真相是:每个 Thread 对象内部自带一张 ThreadLocalMap,数据存在这张 map 里;ThreadLocal 对象本身只是访问地图的钥匙——调用 threadLocal.get(),等价于「拿到当前线程的 map,以这个 ThreadLocal 实例为 key 找值」。同样的代码,不同线程 get 到的是各自的值——因为 map 就长在各自的 Thread 身上。map 的结构还有个关键设计:key 是 ThreadLocal 的弱引用,value 是强引用。记住这个组合,下面的事故全由它引发。
内存泄漏:key 被回收,value 还赖着
key 用弱引用是有意为之:外部代码不再持有 ThreadLocal 实例时,下次 GC 就把它收走,key 变 null——ThreadLocal 对象本身不泄漏。但 value 是强引用,key 没了 value 还牢牢挂在 map 里,null key 的 entry 成了孤儿。更糟的是线程池:线程长期存活反复复用,孤儿 value 只进不出,泄漏被时间放大。ThreadLocalMap 有顺路自清理的逻辑(set、get 时探测清一些),但不保证及时。铁律:用完必须 remove(),写在 finally 里:
static final ThreadLocal<User> CONTEXT = new ThreadLocal<>();
try {
CONTEXT.set(currentUser);
// 业务逻辑,任意深处 CONTEXT.get()
} finally {
CONTEXT.remove(); // 不写这行,线程池里就是定时炸弹
}线程池下的「数据串台」
比泄漏更阴的坑是串台:线程池复用线程,上一个请求 set 的上下文没 remove,下一个请求 get 到的是别人的用户信息——标准的生产事故。串台和泄漏是同一个根源(value 残留),同一剂药方(finally remove)。顺带两句话:InheritableThreadLocal 能把父线程的值传给新建的子线程,但线程池里线程是复用的、不重新创建,不适用;跨线程池传递上下文的通用方案是阿里开源的 TransmittableThreadLocal,知道有这个东西即可。
典型应用与定位
三大常用场景:登录用户上下文(拦截器 set、业务 get、过滤器 finally remove)、数据库连接管理(一个线程一条连接)、SimpleDateFormat 一人一份。最后记住它的定位:ThreadLocal 不是让并发消失,而是让共享消失——没有共享,就没有竞争。它与前面所有的锁是两条独立的路线,不冲突,各管一摊。
显式锁与线程私有数据都齐了。下一篇进入全系列重头戏:线程池——七大参数、一个任务从提交到执行的完整旅程,以及为什么大厂规范明令禁止用 Executors 直接创建。
评论 (0)