编辑
介绍
- 每个Thread对象, 内部有一个ThreadLocalMap threadLocals, 这是一个哈希表, 底层是一个Node[ ] table;
- 当在某个线程中调用ThreadLocal的set方法时, 会使用Thread.currentThread获取当前先线程的thread对象, 然后将ThreadLocal对象作为key, 将set方法的参数作为value, 构建一个Entry, 将此Entry保存到thead对象的ThreadLocalMap中;
- 注意, Entry继承了WeakReference, 在构造方法中, 将ThreadLocal对象传给了WeakReference构造方法, 也就是说ThreadLocalMap中的ThreadLocal作为key, 是被弱引用指向的;
- 这样做, 能保证在主程序中ThreadLocal的引用被置为null后, 对应的threadLocal对象就会被回收, 防止内存泄漏.
- 但是仅仅这样, 还不能防止内存泄漏, ThreadLocal被回收以后, key的值变为null, 但是value还没被回收, 并且value一直有强引用指向; 所以需要调用remove方法, 删除整个Entry;
内存泄漏: 已经不再使用的对象仍然被持有引用,导致垃圾回收器无法回收这些对象的内存,从而导致内存无法释放,逐渐耗尽可用内存。我们在程序中, 应该尽量避免静态的大对象, 避免资源使用不释放, 例如输入输出流
例子
- 考虑一个SpringBoot 后端应用;
- 用户的请求里携带了用户的ID, 我希望在各种不同的地方都能方便地拿到这个Id, 比如在各个Controller里;
- 如果每次都去 Request 中取, 有点麻烦, 我希望能把这个值存起来;
- 做一个Filter, 里面设置一个静态变量, 请求来到的时候, 用这个静态变量来保存用户Id; 以后我直接用 Filter.id 就能拿到了;
- 单线程没问题, 多线程的情况下, 明显有问题: 不同的线程处理不同的请求都会使用同一个 filter 对象, 导致一会存的是这个请求中的userId, 一会又是另一个请求中的userId;
- 这时候, 就可以在 Filter 里设置一个
static ThreadLocal<UserID>
- 假设当前有两个线程; 线程 A 在 Filter 的时候调用 threadLocal.set, 把用户A的Id 保存到了自己 ThreadLocalMap;
- 线程B同理, 把用户B 的 Id 保存到了自己的 TheadLocalMap;
- 虽然不同线程用了同一个
threadLocal
对象作为key, 但 ThreadLocalMap 和 value 是各个线程自己的; - 不同的线程使用相同的
threadLocal
对象去 get, 拿到的是当前线程独有的 value; - 在其他地方就可以通过 Filter.threadLocal.get 去获取本线程的用户Id;
- 不过需要在用完了以后及时回收; 比如在 Filter 里面请求返回的时候调用 threadLocal.remove, 把这次的键值对删除;
- 否则这个线程被分给下个请求的时候, 上个请求的键值对还在; 不过在这个场景下, 不 remove 并不会对内存产生太大危害, 这里 remove 是防止后面的请求查到上一个请求的数据;
下个请求调用 threadLocal.set的时候, 用的还是同一个 threadLocal对象, 新的 Value 会覆盖旧的Value, 旧的Value没有引用指向就回收了
- 内存泄漏主要是考虑到线程长时间存在, 并且运行过程中不断往ThreadLocalMap 里加不同的 ThreadLocal; 如果只有若干个静态的ThreadLocal的话, 其实没啥问题;
- 为什么不直接让用户自己插入键值对到 ThreadLocalMap? 因为自动帮你做了一个 WeakReference;
初始化
- 初始时, 一个Thread对象的ThreadLocalMap threadLocals对象为null, 在该线程中首次调用threadLocal.set或get方法时, 会创建一个Capacity为16的哈希表, 装填因子为2 / 3;
- 如果时threadLocal.get方法触发的创建, 则会放入一个<threadLocal, null>的键值对
- 如果以set方法触发创建, 则放入的是<threadLocal, set方法的参数>
冲突解决
- 和HashMap不同, 使用的是线性探查法, 添加一个Entry时, 如果冲突, 则向后(循环)寻找到第一个null位置, 放到该位置;
ThreadLocal.set
- 计算应该放到的下标, 开始线性探查, 如果过程中遇到key==的结点, 进行value替换
- 如果探查到一个位置为null, 则放入新的Entry;
- 流程:
ThreadLocal的set方法获取currentThread, 拿到currentThread的ThreadLocalMap, 调用这个Map的set方法
ThreadLocal.get
- 线性探查
- 流程总结:
ThreadLocal的get方法获取currentThread, 拿到其threadLocals, 调用它的getEntry方法, 如果getEntry找到了对应的key, 返回对应的value; 如果getEntry返回null, 没找到, 那么将<threadLocal, null>放入ThreadLocalMap, 并返回null;
扩容
- size >= threshold时扩容
- 容量 * 2; 对以前的元素重新hash;
防止内存泄漏
- 两重保险, 一个是弱引用, 一个是remove
- 手动remove, 会直接清除掉整个Entry, table[i] = null;
- 如果没有remove, 但是threadLocal的强引用改为null了, 那么ThreadLocalMap中的弱引用不会阻值垃圾回收, threadLocal对象将被回收, ThreadLocalMap中对应的Entry的key指向null;
- 后续调用ThreadLocalMap的set方法时, 每次都会清除一部分 key == null 的Entry;
- 面试题: 为什么value不设置为弱引用? 一是不能, 二是没必要
- 不能: 当我们通过 threadLocal.set 去 set 一个value 的时候, 很有可能是没有给 value 设置强引用的, 比如
threadLocal.set(new Object())
, 如果 value 也是弱引用, 这时就会出现 key 还在, value 已经被回收的情况, 这样通过 get 方法就只能获取到 null ;- 没必要: 因为调用 threadLocal.set 和 get 时, 进一步调用到 ThreadLocalMap 的 set get 方法, 会对 key == null 的 Entry 进行清除工作, 这样 value 也就一并被清除了;
- 没必要: 提供了remove方法, 只要我们使用规范, 及时 remove, 就能避免造成内存泄漏;
- 面试题: 既然弱引用指向的对象一发生gc就被回收, 我怎么保证用到 threadLocal 对象的时候它还在?
陷阱问题, "弱引用指向的对象一发生gc就被回收"这句话是错误的; 当一个对象没有被强引用指向, 只被弱引用或虚引用指向的时候, gc 才会回收它;