ThreadLocal 线程主变量
前面部分引用其他优秀博客, 后面源码自己分析的, 如有冒犯请私聊我.
用 Java 语言开发的同学对 ThreadLocal 应该都不会陌生, 这个类的使用场景很多, 特别是在一些框架中经常用到, 比如数据库事务操作, 还有 MVC 框架中数据跨层传递. 这里我们简要探讨下 ThreadLocal 的内部实现及可能存在的问题.
首先问自己一个问题, 让自己实现一个这个的功能类的话怎么去做? 第一反应就是简单构造一个 Map<Thread, T> 数据结构, key 是 Thread,value 就是我们要保存的线程变量 T. 我们看下这种设计有哪些问题:
随着运行时间越久, 存在 Map 里的 Thread 越多, 当 Thread 退出时, 资源也没有释放, 存在内存泄漏问题
Map 数据因为会被多线程访问, 存在资源竞争, 所以还必需对 Map 做同步安全操作, 效率低下
JDK 中的 ThreadLocal 精妙的设计来解决问题上述两个问题. 首先每个 Thread(线程)内部都有一个 Map 结构数据 ThreadLocalMap<ThreadLocal, T>, 当我们对线程变量赋值时 ThreadLocal.set(T value)时, 其实是先获取当前线程 Thread.currentThread())的内部属性字段 ThreadLocalMap, 然后以当前 ThreadLocal 为 key 设置线程变量值 T. 这种设计的精髓是, 每个 Thread 线程都维护一份自己的 ThreadLocalMap 数据结构, 这样就解决了上面所述问题中的第二个, 不存在竞争条件.
既然每个 Thread 内部都维护一个 ThreadLocalMap 字典数据结构, 字典的 Key 值是 ThreadLocal, 那么当某个 ThreadLocal 对象不再使用 (没有其它地方再引用) 时, 每个已经关联了此 ThreadLocal 的线程怎么在其内部的 ThreadLocalMap 里做清除此资源呢? JDK 中的 ThreadLocalMap 又做了一次精彩的表演, 它没有继承 java.util.Map 类, 而是自己实现了一套专门用来定时清理无效资源的字典结构. 其内部存储实体结构 Entry<ThreadLocal, T > 继承自 java.lan.ref.WeakReference, 这样当 ThreadLocal 不再被引用时, 因为弱引用机制原因, 当 jvm 发现内存不足时, 会自动回收弱引用指向的实例内存, 即其线程内部的 ThreadLocalMap 会释放其对 ThreadLocal 的引用从而让 jvm 回收 ThreadLocal 对象. 这里是重点强调下, 是回收对 ThreadLocal 对象, 而非整个 Entry, 所以线程变量中的值 T 对象还是在内存中存在的, 所以内存泄漏的问题还没有完全解决. 接着分析 JDK 的实现, 会发现在调用 ThreadLocal.get()或者 ThreadLocal.set(T)时都会定期执行回收无效的 Entry 操作. 所以这就解决了上述问题中的第 1 个问题.
问题真的都解决了吗, 好像都解决了. 因为即没有竞争资源操作, 也不会存在内存泄漏. 但是细想一下, 总感觉哪里不对劲, 真的不会存在内存溢出 (OOM) 问题吗? 上面一段的分析中, 强调 ThreadLocalMap 会定期清理内部的无效 Entry 对象, 触发的条件就是对 TrheadLocal 执行 set,get,remove()等操作时会触发, 但是如果存在这样的场景, 当我们在某个线程上下文中执行 ThreadLocal.set(T)设置了一个很大内存的数据结构, 然后该 ThreadLocal 被清除引用回收, 之前的线程又一直存活着, 则这个大内存数据对象 T 是一直不回收的, 这里很容易写个代码测试出 OOM 来. 怎么解决这个问题呢?
Lucene 中的 org.apache.lucene.util.CloseableThreadLocal 类解决了上述特殊场景引起的问题: 即解决 JDK 中因为定期才执行无效对象回收的问题. CloseableThreadLocal 在内部维护了一个 ThreadLocal, 当执行 CloseableThreadLocal.set(T)时, 内部其实只是代理的把值赋给内部的 ThreadLocal 对象, 即执行 ThreadLocal.set(new WeakReference(T)). 看到这里应该明白了, 这里不是直接存储 T, 则是包装成弱引用对象, 目的就是当内存不足时, jvm 可以回收此对象. 但是细心的你会发现会引入一个新的问题, 即当前线程还存活着的时候, 因为内存不足而回收了弱引用对象, 这样会在下次调用 get()时取不到值返回 null, 这是不可接受的. 所以 CloseableThreadLocal 在内部还创建了一个数据, WeakHashMap<Thread, T>, 当线程只要存活时, 则 T 就至少有一个引用存在, 所以不会被提前回收. 但是又引入的第 2 个问题, 对 WeakHashMap 的操作要做同步 synchronized 限制. 你看, 所有的东西都不是十全十美的, 我们掌握那个平衡点就行了.
ThreadLocal 源码分析
源码介绍
1, 每个线程访问自己维护的的一份副本
2,ThreadLocal 类内部维护了一个私有的字段去跟对应的线程联系(例如 userid 或者 TranslationId) 这个字段第一次调用时生成后面调用不会改变
3, 只要这个线程活着而且实例可以被访问, 这个线程会持有这个变量副本的隐性引用, 直到线程消亡, 被垃圾回收
ThreadLocal get 方法
1, 根据当前 thread 获取对应的 ThreadLocalMap 如果没有就初始化设置一个, 如果有就返回 ThreadLocalMap 里面维护的 Entry 存储的值
- public T get() {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null) {
- @SuppressWarnings("unchecked")
- T result = (T)e.value;
- return result;
- }
- }
- return setInitialValue();
- }
ThreadLocal 初始化 value
俩个步骤 1, 在原有的 map 上设置值 2, 创建一个 ThreadLocalMap
- private T setInitialValue() {
- T value = initialValue();
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- return value;
- }
ThreadLocal rehash 1, 先删除陈旧的 Entriy , 如果不能有效的收缩 table 的长度, 而且长度已经大于 threshold 的 0.75(装载因子)倍了, 就直接扩展一倍长度
- /**
- * Re-pack and/or re-size the table. First scan the entire
- * table removing stale entries. If this doesn't sufficiently
- * shrink the size of the table, double the table size.
- */
- private void rehash() {
- expungeStaleEntries();
- // Use lower threshold for doubling to avoid hysteresis
- if (size>= threshold - threshold / 4)
- resize();
- }
如果你也有此类问题, 可以一起探讨(私聊或者评论), 一起不断完善自己的理解, 如果觉得可以欢迎关注我.
来源: https://www.cnblogs.com/xushengyong/p/10641827.html