博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
JDK ThreadLocal解析
阅读量:5328 次
发布时间:2019-06-14

本文共 5968 字,大约阅读时间需要 19 分钟。

Java ThreadLocal解析

ThreadLocal 线程本地变量, 线程私有, 在 Thread 类中用 ThreadLocal.ThreadLocalMap threadLocals 以数组的形式存储. 因为是线程私有的变量, 所以不会有多线程访问的线程安全问题, 下面就对它开始解析(JDK1.8 的 ThreadLocal).

threadLocalHashCode

在说 ThreadLocal 的使用之前, 先说一个比较重要的东西, 就是 threadLocalHashCode, 这个hashCode 的值用来计算 当前 ThreadLocal 应当位于 ThreadLocalMap 的数组中的下标, 为什么是应当呢, 请往下看

// ThreadLocalHashCode 递增值    private static final int HASH_INCREMENT = 0x61c88647;  /**   * Returns the next hash code.   */  private static int nextHashCode() {    return nextHashCode.getAndAdd(HASH_INCREMENT);  }    // 每个 ThreadLocal 的 threadLocalHashCode 通过一个静态的原子变量递增而获得    private final int threadLocalHashCode = nextHashCode();

ThreadLocal#set 方法

一个 ThreadLocalMap 可以存储多个 ThreadLocal 变量, 存取的时候将 ThreadLocal 作为key 放入 Entry 中, ThreadLocalMap 存储数据的对象为 Entry[], 计算 key 所属的下标用 int i = key.threadLocalHashCode & (len-1) 计算, 如果该下标已有数据, 根据 key(ThreadLocal变量) 判断是否属于这个 ThreadLocal, 不是同一个 ThreadLocal 则按顺序往后查找为空(或者 ThreadLocal 已经被GC回收)的 Entry设置 value.下面是 set方法的比较重要的部分源代码:

// 最开始计算的下标i    int i = key.threadLocalHashCode & (len-1);    // 从计算出来的数组中的第 i 个开始往后找, i > len-1 的时候就从0开始  for (Entry e = tab[i];       e != null;       e = tab[i = nextIndex(i, len)]) {    // nextIndex(i,len): ((i + 1 < len) ? i + 1 : 0); i+1>=len则从下标0开始    ThreadLocal
k = e.get(); // 根据 key 判断是否为当前的 ThreadLocal if (k == key) { e.value = value; return; } // Entry 的 key 为空, 但是上面循环的判断是 Entry 不为空, 表明作为 Entry 的 key的 ThreadLocal 已经被 GC 回收, 所以此时设置新的 key(ThreadLocal) 和 value. if (k == null) { // 替换掉旧的 ThreadLocal replaceStaleEntry(key, value, i); return; } // 已经找到为空的 Entry, 直接 new 一个 Entry tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) // 扩容将所有的 Entry 重新设置 rehash(); }

ThreadLocal#get 方法

get 方法跟 set 方法还是有挺多相同处的, 看代码

private Entry getEntry(ThreadLocal
key) { // 计算下标 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; // 如果对应下标的 Entry 属于当前 ThreadLocal 的话, 直接返回结果 if (e != null && e.get() == key) return e; else // 会到这里的原因是因为 set 的时候的 hash 冲突, 冲突后会往后查找(超过len-1则从0开始)可以放置的位置, 所以前面初始化 hashCode 的时候是用的应当位于的位置 return getEntryAfterMiss(key, i, e); } // e 为应当属于它的 Entry private Entry getEntryAfterMiss(ThreadLocal
key, int i, Entry e) { Entry[] tab = table; int len = tab.length; // while (e != null) { ThreadLocal
k = e.get(); // 查找到属于自己的 value if (k == key) return e; // 如果在查找的时候发现有的 Entry 的 key(ThreadLocal) 已经被 GC 回收, 则进行回收 if (k == null) expungeStaleEntry(i); else // 往后查找, 超过 len-1 则从 0 开始继续 i = nextIndex(i, len); e = tab[i]; } return null; }

关于ThreadLocal 的 WeakReference

在说 ThreadLocal 的 WeakReference 之前, 先简单介绍一下 WeakReference .

WeakReference

弱引用不同于 java 中用 new创建(强引用) 的对象, 当对象只有弱引用(weakReference) 的时候, 进行GC的时候, 会对其进行回收(没有强引用的对象均会被gc回收)

Object o = new Object();  WeakReference weakReference = new WeakReference(o);  System.out.println("beforeGC: "+weakReference.get());  o = null;  System.gc();  System.out.println("afterGC: "+weakReference.get());  Object o2 = new Object();  WeakReference weakReference2 = new WeakReference(o2);  System.out.println("beforeGC2: "+weakReference2.get());  System.gc();  System.out.println("afterGC2: "+weakReference2.get());

输出结果是:

beforeGC: java.lang.Object@5e2de80c

afterGC: null
beforeGC: java.lang.Object@1d44bcfa
afterGC: java.lang.Object@1d44bcfa

从这里可以看出, 即使有 weakFerence 弱引用, 也不能阻止对象被 GC 回收掉(因为它很弱啊哈哈).

ThreadLocal 中使用的 WeakReference

从上面set的方法看到了一行 tab[i] = new Entry(key, value)

Entry 是一个静态类

// Entry 继承 WeakReference, 多了个 value 记录 ThreadLocal 的值    static class Entry extends WeakReference
> { // 这个值就是 ThreadLocal set 进来的值 Object value; Entry(ThreadLocal
k, Object v) { // 将 ThreadLocal 设置为引用对象 super(k); value = v; } }

super(k)就是将 ThreadLocal 设置为引用对象, 获取的时候,调用父类的get方法, entry.get() --> 返回的就是引用的对象(这里就是 ThreadLocal)

// super(k)    Reference(T referent) {    this(referent, null);  }  // set reference  Reference(T referent, ReferenceQueue
queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; } // get public T get() { return this.referent; }

代码看起来难度不大, 下面说一下 JDK 的 ThreadLocal 使用不当会导致内存泄漏的问题.

在上面的 set 和 get 方法中, 有这么(类似)一段代码:

for (Entry e = tab[i];       e != null;       e = tab[i = nextIndex(i, len)]) {    // nextIndex(i,len): ((i + 1 < len) ? i + 1 : 0); i+1>=len则从下标0开始    ThreadLocal
k = e.get(); // 根据 key 判断是否为当前的 ThreadLocal if (k == key) { e.value = value; return; } // Entry 的 key 为空, 但是上面循环的判断是 Entry 不为空, 表明作为 Entry 的 key的 ThreadLocal 已经被 GC 回收, 所以此时设置新的 key(ThreadLocal) 和 value. if (k == null) { // 替换掉旧的 ThreadLocal replaceStaleEntry(key, value, i); return; }

if(k == null) 这里, 加上外层循环就是 if(e!=null && k == null), Entry不为空表明之前设置过 ThreadLocal 的值, 但是 k(key)==null 也就是 ThreadLocal == null, 为什么ThreadLocal 会为空? 前面我们说到 Entry 里面对 ThreadLocal(key) 是使用的 弱引用 , 也就是 Entry 持有 ThreadLocal 的引用并不会阻止GC 将 ThreadLocal 回收掉, 如果 ThreadLocal 手动置为 null , 则失去了强引用, 若其他地方没有这个 ThreadLocal 的强引用, 这个ThreadLocal 将会被GC回收掉, 此时 Entry 里面的 key == null, 但是 Entry 对 value 是强引用, 所以 value 不会被GC回收掉, 此时就造成了 Entry 的 key==null 但是 value !=null, 如果没有其它的 ThreadLocal 被设置进(或者get)同一个 Entry 的话, 这个 Entry 将会一直持有无用的 value, 造成内存泄漏, 泄漏的过多最终会导致 OOM.

解决办法呢就是在 ThreadLocal 置为 null之前, 先调用 remove 方法:

private void remove(ThreadLocal
key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 找到与自己匹配的 Entry if (e.get() == key) { e.clear(); // 这个方法名也说的很清楚了, 删掉陈旧的 Entry expungeStaleEntry(i); return; } } }

所以使用 JDK 的 ThreadLocal , 在不需要的时候需要手动调用 remove() 方法, 防止导致内存泄漏, 最终导致 OOM.

转载于:https://www.cnblogs.com/wuhaonan/p/11427119.html

你可能感兴趣的文章
Hyper-V虚拟机上安装一个图形界面的Linux系统
查看>>
js千分位处理
查看>>
字符串类型的相互转换
查看>>
基础学习:C#中float的取值范围和精度
查看>>
Vim配置Node.js开发工具
查看>>
web前端面试题2017
查看>>
ELMAH——可插拔错误日志工具
查看>>
MySQL学习笔记(四)
查看>>
【Crash Course Psychology】2. Research & Experimentation笔记
查看>>
SOPC Builder中SystemID
查看>>
关于 linux 的 limit 的设置
查看>>
HDU(4528),BFS,2013腾讯编程马拉松初赛第五场(3月25日)
查看>>
vim中文帮助教程
查看>>
MySQL基础3
查看>>
RxJS & Angular
查看>>
面向对象(多异常的声明与处理)
查看>>
MTK笔记
查看>>
ERROR: duplicate key value violates unique constraint "xxx"
查看>>
激活office 365 的启动文件
查看>>
无法根据中文查找
查看>>