ThreadLocal解析
ThreadLocal 是什么?
ThreadLocal 不是“线程”,也不是“存储容器”,而是一个“访问代理”或“操作工具类”。
可以把它理解为:
一个“钥匙生成器”+“存取接口”——它本身不存数据,但是可以帮我在线程专属的存储空间里面存取数据
关键点:
ThreadLocal每个实例代表一个独立的“变量槽位”- 它通过
set(value)/get()/remove()方法,操作当前线程内部的一个 Map(即ThreadLocalMap) - 所有数据实际存储在
Thread对象内部,而不是ThreadLocal里
ThreadLocal 和 ThreadLocalMap 的关系
它们的结构如下:
1 | Thread |
关系总结
| 角色 | 所在位置 | |
|---|---|---|
ThreadLocal |
操作入口,每个实例代表一个“变量名” | 全局(通常是 static) |
ThreadLocalMap |
真正的存储容器,是一个自定义哈希表 | 属于 Thread 对象(每个线程一个) |
Thread |
宿主,持有自己的 ThreadLocalMap |
JVM 线程 |
ThreadLocal结构
起初我以为ThreadLocal是一个大的map,线程当key变量当value。
实际上错了,因为那样ThreadLocal自己又变成共享变量了,还得加锁。
真正的实现是
每个线程内部都有一个ThreadLocalMap,key是ThreadLocal对象,value是线程独立数据
如下图
这样线程1和线程2去访问threadlocal1时,查找的都是自己的ThreadLocalMap
为什么能实现“线程隔离”
答: 每个线程都有自己的ThreadLocalMap,彼此完全独立。
在 线程 A 中调用 userId.set("A"),数据存入 线程 A 的 map;
在 线程 B 中调用 userId.set("B"),数据存入 线程 B 的 map。
他们操作的属于不同的内存区域,自然互补干扰。
数据绑定到线程自身,而非共享全局变量。
为什么能实现“线程内资源共享”
答:在一个线程中,所以代码(不管经历几层的方法调用),都能通过一个ThreadLocal实例,访问到当前线程map中对应的数据。
如ThreadLocalDemo.java中所示。
源码关键片段
ThreadLocal.set()
1 | public void set(T value) { |
ThreadLocal.get()
1 | public T get() { |
内存泄漏风险
内存泄漏
(英语:memory leak)是计算机科学中的一种资源泄漏,主因是计算机程序的内存管理失当,因而失去对一段已分配内存空间的控制,程序继续占用已不再使用的内存空间,或是存储器所存储之对象无法透过执行代码而访问,令内存资源空耗。 -维基百科
先说结论:会
但是要注意:Java一般用来开发web,web一般用tomcat,所以大部分情况下业务是跑在tomcat线程池上的,所以ThreadLocal内存泄漏的问题要建立在线程会被复用的线程池场景。
源码分析
1 | static class ThreadLocalMap { |
因为Entry 继承自
WeakReference,所以Entry本身就是一个弱引用对象。entry对象通过弱引用关联当前的
threadLocal对象,通过强引用value保存我们设置的值。此外,还有栈上的
threadLocal引用指向堆中的threadLocal对象,这个引用是强引用
如下图
并且,如果有红色箭头指向的强引用存在的话,说明目前
threadlocal是有用的,如果发生GC,threadlocal对象不会被清楚。但如果threadlocal是成员变量(也就是非静态)随着方法的执行完成,相应的栈帧也出栈了,此时,红色箭头指向的强引用链就没了。再后来,如果没有别的栈对
threadlocal对象有引用的话,它对应的entry就没法再被访问到了,此时内存泄漏就发生了。那如果
ThreadLocal被定义成静态变量呢?照理说有静态变量的强引用在,因此
ThreadLocal中的key弱引用不会被GC回收,所以不会有内存泄漏。但是这是不对的,一般情况下确实不会,但是类加载器是可以被卸载的,此时对应的类会被回收,于是强引用又没了…
不过ThreadLocal本身也对内存泄漏问题做了防范,我们以get方法作为切入
1 | public T get() { |
以上虽然降低了内存泄漏的发生概率,但这其实就是把清理的开销,弄到了get和set上。万一get的时候清理的无用的entry特别多,那这次get相对而言就比较慢了。
并且,这个方法只能介绍内存泄漏的发生,因为这种清理是局部的,它是从触发清理的位置开始向右遍历,直到碰到第一个有效的entry,所以不是所有的entry都会被检查,万一运气就不好呢?
最佳实践
不要滥用
ThreadLocal普通参数不要用它,它适合存放线程独立信息如
- 数据库链接
- 用户上下文
用完一定要
remove,在try-finally里面清理把
ThreadLocal定义成静态变量,使它全局可用,只分配一块内存