ThreadLocal是大家比较常用到的,在多线程下存储线程相关数据十分合适。可是很多时候我们并没有深入去了解它的原理。
首选提出几个问题,稍后再针对这些问题一一解答。
进入正题,先简单了解下ThreadLocal 和 Thread,ThreadLocal的类结构:
可以看到,ThreadLocal有个内部类ThreadLocalMap,ThreadLocalMap又有个内部类Entry。
Thread类有这样一段源码:
class Thread implements Runnable { ...省略若干代码 /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
通过Thread源码我们了解到,Thread持有的对象是 ThreadLocal 的 ThreadLocalMap,这一点特别重要,线程相关数据都是通过 ThreadLocalMap 存储的,而不是 ThreadLocal。
此时我们得到的结论如下图所示:
Thread的 threadLocals 属性直接关联的ThreadLocal.ThreadLocalMap,和ThreadLocal没有丝毫关系
那么 ThreadLocal是做什么的呢?其实 ThreadLocal可以看做线程操作 ThreadLocalMap的工具类, ThreadLocal暴漏了两个公共方法get()和set(T)用来获取和设置 ThreadLocalMap。
了解一下set方法源码:
1 public void set(T value) { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) 5 map.set(this, value); 6 else 7 createMap(t, value); 8 }
从源码第五行我们可以得到两个重要的信息:
getMap(t)方法的实现很简单:
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
它返回的是 Thread的 threadLocals 属性 ,代码上验证了: “ 线程局部变量 ”是存储在Thread对象的 threadLocals属性中 ,和 ThreadLocal 本身 没什么关系。 ThreadLocal 可以当做 访问的工具类。
这里我们第2个问题:ThreadLocal是如何做到可以当做线程局部变量的已经有答案啦,所有的操作其实都是对Thread 下 threadLocals 的操作,所以跨线程操作也不会产生问题的,因为getMap()永远返回当前线程的 threadLocals 属性。
小伙伴们是不是很奇怪为什么要把this当做key呢?这就扯到我们文章开头的第一个问题了:弱引用!
跟进 map.set(this, value);源码一看究竟:
1 private void set(ThreadLocal<?> key, Object value) { 2 3 Entry[] tab = table; 4 int len = tab.length; 5 int i = key.threadLocalHashCode & (len-1); 6 7 for (Entry e = tab[i]; 8 e != null; 9 e = tab[i = nextIndex(i, len)]) { 10 ThreadLocal<?> k = e.get(); 11 12 if (k == key) { 13 e.value = value; 14 return; 15 } 16 17 if (k == null) { 18 replaceStaleEntry(key, value, i); 19 return; 20 } 21 } 22 23 tab[i] = new Entry(key, value); 24 int sz = ++size; 25 if (!cleanSomeSlots(i, sz) && sz >= threshold) 26 rehash(); 27 }
查看23行Entry的构造方法:
1 static class Entry extends WeakReference<ThreadLocal<?>> { 2 /** The value associated with this ThreadLocal. */ 3 Object value; 4 5 Entry(ThreadLocal<?> k, Object v) { 6 super(k); 7 value = v; 8 } 9 }
Entry只有一个构造方法,该构造方法接受两个参数k和v,k就是当前ThreadLocal对象,v是我要存储的线程相关数据。通过上述代码标红部分我们可以了解到 对 k 使用了弱引用 ,但是value不是, value是强引用 。至此第一个问题已经真相了,大家所说的ThreadLocal弱引用其实是ThreadLocalMap和ThreadLocal是弱引用关系。
为什么要这么设计呢?
首选我们整理下当前引用关系如下图:
value一般是线程相关的数据,线程回收后value -> null,强引用就不存在了。但是ThreadLocal对象的生命周期不一定和线程相关,可能线程消亡后ThreadLocal对象仍然被其它线程引用,如果使用强引用的话,ThreadLocalMap对象就无法释放内存,发生内存泄漏的情况。使用弱引用就安全的多了,发生gc时弱引用指向的对象会被内存回收。
问题1和2已经在上文中提到,继续看问题3,创建ThreadLocal对象时为什么要用static修饰呢?
个人感觉是基于两点的考虑:
网上很多地方把static和内存泄漏联系起来,原谅我没看出来这两者有什么关系。
最后来到第四个问题,也大家都关心的内存泄漏啦,。
通过上面的引用关系图我们了解到存在两个引用关系,分别是key的弱引用和value的强引用。弱引用首选不可能导致内存泄漏,因为gc发生时弱引用的对象就有可能被回收了。所以。。。内存泄漏发生在强引用这个关系上。
因为现在线程切换的开销比较大,大家现在普遍使用线程池的技术去避免线程的频繁创建。在线程池中,线程不会消亡,会被重复使用,so。。。。上边的强引用得不到释放了,内存泄漏就这样发生了。其实我在JDK8上看到的是java已经为此做了一些工作了,比如执行下次set操作时遍历key是null的Entry对象并释放value的引用。虽然java本身做了一些工作,仍然强烈建议使用完ThreadLocal执行remove方法主动消除引用关系。
文章结束了,如有纰漏,欢迎指出。