1.测试代码
public class ThreadLocalTest { public static void main(String[] args) { ThreadLocal<String> threadLocal = new ThreadLocal<>(); threadLocal.set("郑钦锋"); } }
public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程关联的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) # set值, this: 指ThreadLocal map.set(this, value); else createMap(t, value); }
下面分析set(ThreadLocal tl,Object value)方法
private void set(ThreadLocal<?> key, Object value) { # 底层使用Entry数组存储数据 Entry[] tab = table; # 默认16 int len = tab.length; # 计算槽位 int i = key.threadLocalHashCode & (len-1); # 从数组中获取key对应槽位的值,一个Entry对象 for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) { # Entry对象的key就是ThreadLocal对象 ThreadLocal<?> k = e.get(); # 如果从槽位中获取的Entry的k 等于 传入的key,将新值赋到这个Entry上面,然后结束set方法 if (k == key) { e.value = value; return; } # 如果槽中Entry的key为空,最终会调用expungeStaleEntry方法移除这个槽中的数据 if (k == null) { replaceStaleEntry(key, value, i); return; } } # 回到正常逻辑 # 如果槽位i是空,创建一个Entry对象,并将这个Entry对象放到i这个槽位上 tab[i] = new Entry(key, value); int sz = ++size; # 底层调用expungeStaleEntry方法移除槽中key(ThreadLocal)为null对应的Value值,同时清空这个槽位 if (!cleanSomeSlots(i, sz) && sz >= threshold) # 扩容 rehash(); }
看了上面代码注释,我们有必要分析以下两点:
(1) 创建Entry对象的具体的过程
(2) expungeStaleEntry方法是如何清空key为null的Entry对象的
接下来先看第1点,如何创建Entry?
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
代码其实很简单,就是将key交给顶级父类Reference处理,一直跟踪代码会看到下面这行代码
private T referent; /* Treated specially by GC */ 真惨
接着看expungeStaleEntry方法,这也很简单,结合着cleanSomeSlots方法来看,直接上代码
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]; # 如果key为null,删除 if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; }
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot 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)) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { 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; }
ok!
最后总结一下:
别人写的代码就是复杂,对象是包了一层又一层,ThreadLocal就是下面这模样
(1) 大家可以看到,Entry的key是一个弱引对象,这样的对象在GC有可能被回收掉,但是Entry的Value值(就是我们set的值)呢,它在一条引用链上,如果本线程没有被干掉,它是不会被回收掉的。而且因为key又是null, 永远都不会被使用了,一个僵尸对象! 这样导致内存泄漏也就不难理解了
(2)这么明显的漏洞,ThreadLocal肯定也注意到了,所以它在set,get,remove方法时,都会对key为null的Entry对象进行清理
(3) 内存泄漏的根本原因是: Thread---> ThreadLocalMap---> Entry在一条引用链上,Thread不死,后面的也死不了!
记录: FastThreadLocal
听说很牛逼,效率很高,结果去官网上一看,它果然很无耻写到:
这是一个ThreadLocal的变体, 使用它可以产生更高的访问性能, 它使用的是常量index,而不像ThreadLocal使用hashCode index去检索数组中的数据!
我觉得单是这一点,两者差别应该不大,时间复杂度都是圈1, 应该还有其它的原因,留着以后学习吧!