标签:rmi 除了 也有 result 这不 设置 with log ash
ThreadLocal 也是一个使用频率较高的类,在框架中也经常见到,比如 Spring。
有关 ThreadLocal 源码分析的文章不少,其中有个问题常被提及:ThreadLocal 是否存在内存泄漏?
不少文章对此讲述比较模糊,经常让人看完脑子还是一头雾水,我也有此困惑。因此找时间跟小伙伴讨论了一番,总算对这个问题有了一定的理解,这里记录和分享一下,希望对有同样困惑的朋友们有所帮助。当然,若有理解不当的地方也欢迎指正。
啰嗦就到这里,下面先从 ThreadLocal 的一个应用场景开始分析吧。
ThreadLocal 的应用场景不少,这里举个简单的例子:单点登录拦截。
也就是在处理一个 HTTP 请求之前,判断用户是否登录:
先定义一个 UserInfoHolder 类保存用户的登录信息,其内部用 ThreadLocal 存储,示例如下:
public class UserInfoHolder { private static final ThreadLocal<Map<String, String>> USER_INFO_THREAD_LOCAL = new ThreadLocal<>(); public static void set(Map<String, String> map) { USER_INFO_THREAD_LOCAL.set(map); } public static Map<String, String> get() { return USER_INFO_THREAD_LOCAL.get(); } public static void clear() { USER_INFO_THREAD_LOCAL.remove(); } // ... }
通过 UserInfoHolder 可以存储和获取用户的登录信息,以便在业务中使用。
Spring 项目中,如果我们想在处理一个 HTTP 请求之前或之后做些额外的处理,通常定义一个类继承 HandlerInterceptorAdapter,然后重写它的一些方法。举例如下(仅供参考,省略了一些代码):
public class LoginInterceptor extends HandlerInterceptorAdapter { // ... @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // ... // 请求执行前,获取用户登录信息并保存 Map<String, String> userInfoMap = getUserInfo(); UserInfoHolder.set(userInfoMap); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 请求执行后,清理掉用户信息 UserInfoHolder.clear(); } }
在本例中,我们在处理一个请求之前获取用户的信息,在处理完请求之后,将用户信息清空。应该有朋友在框架或者自己的项目中见过类似代码。
下面我们深入 ThreadLocal 的内部,来分析这些方法做了些什么,跟内存泄漏又是怎么扯上关系的。
先从头开始,也就是类签名:
public class ThreadLocal<T> { }
可见它就是一个普通的类,并没有实现任何接口、也无父类继承。
ThreadLocal 只有一个无参构造器:
public ThreadLocal() { }
此外,JDK 1.8 引入了一个使用 lambda 表达式初始化的静态方法 withInitial,如下:
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier); }
该方法也可以初始化一个对象,和构造器也比较接近。
ThreadLocalMap 是 ThreadLocal 的一个内部嵌套类。
由于 ThreadLocal 的主要操作实际都是通过 ThreadLocalMap 的方法实现的,因此先分析 ThreadLocalMap 的主要代码:
public class ThreadLocal<T> { // 生成 ThreadLocal 的哈希码,用于计算在 Entry 数组中的位置 private final int threadLocalHashCode = nextHashCode(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } // ... static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // 初始容量,必须是 2 的次幂 private static final int INITIAL_CAPACITY = 16; // 存储数据的数组 private Entry[] table; // table 中的 Entry 数量 private int size = 0; // 扩容的阈值 private int threshold; // Default to 0 // 设置扩容阈值 private void setThreshold(int len) { threshold = len * 2 / 3; } // 第一次添加元素使用的构造器 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } // ... } }
ThreadLocalMap 的内部结构其实跟 HashMap 很类似,可以对比前面「JDK源码分析-HashMap(1)」对 HashMap 的分析。
二者都是「键-值对」构成的数组,对哈希冲突的处理方式不同,导致了它们在结构上产生了一些区别:
其它部分大体是类似的。
有个值得注意的地方是:ThreadLocalMap 的 Entry 继承了 WeakReference 类,也就是弱引用类型。
跟进 Entry 的父类,可以看到 ThreadLocal 最终赋值给了 WeakReference 的父类 Reference 的 referent 属性。即,可以认为 Entry 持有了两个对象的引用:ThreadLocal 类型的「弱引用」和 Object 类型的「强引用」,其中 ThreadLocal 为 key,Object 为 value。如图所示:
ThreadLocal 在某些情况可能产生的「内存泄漏」就跟这个「弱引用」有关,后面再展开分析。
Entry 的 key 是 ThreadLocal 类型的,它是如何在数组中散列的呢?
ThreadLocal 有个 threadLocalHashCode 变量,每次创建 ThreadLocal 对象时,这个变量都会增加一个固定的值 HASH_INCREMENT,即 0x61c88647,这个数字似乎跟黄金分割、斐波那契数有关,但这不是重点,有兴趣的朋友可以去深入研究下,这里我们知道它的目的就行了。与 HashMap 的 hash 算法的目的近似,就是为了散列的更均匀。
下面分析 ThreadLocal 的主要方法实现。
ThreadLocal 主要有三个方法:set、get 和 remove,下面分别介绍。
public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 从 Thread 中获取 ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
threadLocals 是 Thread 持有的一个 ThreadLocalMap 引用,默认是 null:
public class Thread implements Runnable { // 其他代码... ThreadLocal.ThreadLocalMap threadLocals = null; }
若从当前 Thread 拿到的 ThreadLocalMap 为何,表示该属性并未初始化,执行 createMap 初始化:
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
若已存在,则调用 ThreadLocalMap 的 set 方法:
private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; // 1. 计算 key 在数组中的下标 i int i = key.threadLocalHashCode & (len-1); // 1.1 若数组下标为 i 的位置有元素 // 判断 i 位置的 Entry 是否为空;不为空则从 i 开始向后遍历数组 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 索引为 i 的元素就是要查找的元素,用新值覆盖旧值,到此返回 if (k == key) { e.value = value; return; } // 索引为 i 的元素并非要查找的元素,且该位置中 Entry 的 Key 已经是 null // Key 为 null 表明该 Entry 已经过期了,此时用新值来替换这个位置的过期值 if (k == null) { // 替换过期的 Entry, replaceStaleEntry(key, value, i); return; } } // 1.2 若数组下标为 i 的位置为空,将要存储的元素放到 i 的位置 tab[i] = new Entry(key, value); int sz = ++size; // 若未清理过期的 Entry,且数组的大小达到阈值,执行 rehash 操作 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
先总结下 set 方法主要流程:
首先根据 key 的 threadLocalHashCode 计算它的数组下标:
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // 从 staleSlot 开始向前遍历,若遇到过期的槽(Entry 的 key 为空),更新 slotToExpunge // 直到 Entry 为空停止遍历 int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // 从 staleSlot 开始向后遍历,若遇到与当前 key 相等的 Entry,更新旧值,并将二者换位置 // 目的是把它放到「应该」在的位置 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == key) { // 更新旧值 e.value = value; // 换位置 tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot // 若未找到 key,说明 Entry 此前并不存在,新增 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
replaceStaleEntry 的主要执行流程如下:
PS: 所谓 Entry「应该」在的位置,就是根据 key 的 threadLocalHashCode 与数组长度取余计算出来的位置,即 k.threadLocalHashCode & (len - 1) ,或者哈希冲突之后的位置,这里只是为了方便描述。
// staleSlot 表示过期的槽位(即 Entry 数组的下标) private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 1. 将给定位置的 Entry 置为 null tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; // 遍历数组 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { // 获取 Entry 的 key ThreadLocal<?> k = e.get(); if (k == null) { // 若 key 为 null,表示 Entry 过期,将 Entry 置空 e.value = null; tab[i] = null; size--; } else { // key 不为空,表示 Entry 未过期 // 计算 key 的位置,若 Entry 不在它「应该」在的位置,把它移到「应该」在的位置 int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
该方法主要做了哪些工作呢?
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; // Entry 不为空、key 为空,即 Entry 过期 if (e != null && e.get() == null) { n = len; removed = true; // 清理 i 后面连续过期的 Entry,直到 Entry 为 null,返回该 Entry 的下标 i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; }
该方法做了什么呢?从给定位置的下一个开始扫描数组,若遇到 key 为空的 Entry(过期的),则清理该位置及其后面过期的槽。
值得注意的是,该方法循环执行的次数为 log(n)。由于该方法是在 set 方法内部被调用的,也就是新增/更新时:
因此,这个次数其实就一种平衡策略:Entry 数组较小时,就少清理几次;数组较大时,就多清理几次。
private void rehash() { // 清理数组中过期的 Entry expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 4) resize(); } // 从头开始清理整个 Entry 数组 private void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { Entry e = tab[j]; if (e != null && e.get() == null) expungeStaleEntry(j); } }
该方法主要作用:
/** * Double the capacity of the table. */ private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; // 新长度为旧长度的两倍 Entry[] newTab = new Entry[newLen]; int count = 0; // 遍历旧的 Entry 数组,将数组中的值移到新数组中 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); // 若 Entry 的 key 已过期,则将 Entry 清理掉 if (k == null) { e.value = null; // Help the GC } else { // 计算在新数组中的位置 int h = k.threadLocalHashCode & (newLen - 1); // 哈希冲突,线性探测下一个位置 while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } // 设置新的阈值 setThreshold(newLen); size = count; table = newTab; }
该方法的作用是 Entry 数组扩容,主要流程:
分析完了 set 方法,再看 get 方法就相对容易了不少。
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(); }
get 方法首先获取当前线程的 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; } protected T initialValue() { return null; }
选取初始值,这个初始值默认为空(该方法是 protected,可以由子类初始化)。
除了初始值,其他逻辑跟 set 方法是一样的,这里不再赘述。
PS: 可以看到初始值是惰性初始化的。
private Entry getEntry(ThreadLocal<?> key) { // 计算下标 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; // 查找命中 if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); } // key 未命中 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; // 遍历数组 while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; // 是要找的 key,返回 if (k == null) expungeStaleEntry(i); // Entry 已过期,清理 Entry else i = nextIndex(i, len); // 向后遍历 e = tab[i]; } return null; }
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
这里调用了 ThreadLocalMap 的 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)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
其中 e.clear 调用的是 Entry 的父类 Reference 的 clear 方法:
public void clear() { this.referent = null; }
其实就是将 Entry 的 key 置空。
remove 方法的主要执行流程如下:
ThreadLocal 的主要方法 set、get 和 remove 前面已经分析过,这里简单做个小结。
set 方法
get 方法
remove 方法:以当前 ThreadLocal 为 key,从 Entry 数组清理掉对应的 Entry,并且在清理该位置后面的、过期的 Entry
方法虽少,但是稍微有点绕,除了做本身的功能,都执行了一些额外的清理操作。
分析了这几个方法的源码之后,下面就来研究一下内存泄漏的问题。
首先说明一点,ThreadLocal 通常作为成员变量或静态变量来使用(也就是共享的),比如前面应用场景中的例子。因为局部变量已经在同一条线程内部了,没必要使用 ThreadLocal。
为便于理解,这里先给出了 Thread、ThreadLocal、ThreadLocalMap、Entry 这几个类在 JVM 的内存示意图:
简单说明:
若方法执行完毕、线程正常消亡,则 Thread 的 ThreadLocalMap 引用将断开,如图:
以后 GC 发生时,弱引用也会断开,整个 ThreadLocalMap 都会被回收掉,不存在内存泄漏。
如果是线程池中的线程呢?也就是线程一直存活。经过 GC 后 Entry 持有的 ThreadLocal 引用断开,Entry 的 key 为空,value 不为空,如图所示:
此时,如果没有任何 remove 或者 get 等清理 Entry 数组的动作,那么该 Entry 的 value 持有的 Object 就不会被回收掉。这样就产生了内存泄漏。
这种情况其实也很容易避免,使用完执行 remove 方法就行了。
本文分析了 ThreadLocal 的主要方法实现,并分析了它可能存在内存泄漏的场景。
标签:rmi 除了 也有 result 这不 设置 with log ash
原文地址:https://www.cnblogs.com/hulianwangjiagoushi/p/13347214.html