1、首先我们定义一个对象,并重写了initialValue()方法来设置ThreadLocal的初始值:
private static ThreadLocal<String> serviceNumberCache = new ThreadLocal<String>() { @Override protected String initialValue() { return "0000"; }; };
2、设置Value值
serviceNumberCache.set("10001");
代码比较简单,话不多说,直接上对应源码:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
getMap() 源码:
ThreadLocalMap getMap(Thread t) { //这里是获取Thread里面的变量 return t.threadLocals; } threadLocals对象定义:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ //这个是在Thread类里面 ThreadLocal.ThreadLocalMap threadLocals = null;
createMap()方法:
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
在赋值时,首先获取当前线程 t,然后调用getMap()方法,获取ThreadLocalMap,如果存在就赋值,不存在就创建一个新的ThreadLocalMap.
注意了:getMap() 方法返回的是 当前线程的一个属性(t.threadLocals),是存放的每个线程自己的东西,和其他线程不相干,这就很好的解释了ThreadLocal 和多线程共享一点关系都没有,ThreadLocal是各自线程取自己里面的东西,根本不存在共享问题,就是自己私有的。我们再看一下createMap()方法进一步验证,创建时也是将新增的 ThreadLocalMap 赋值给线程自己的对象 threadLocals,说明每一个Thread维护一个自己的ThreadLocalMap映射表,new ThreadLocalMap(this,firstValue),这里说明了 我们定义的ThreadLocat 是作为映射表里面的key,需要存储的值作为value.这样也证明了我们原先的理解(存放在一个Map 里面,Key 是 线程,Value 是需要保存的 值) 确实是错误的,这里并不存在一个共享变量.
总结一下ThreadLocal的设计思路:
每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。
这样设计的主要有以下几点优势:
这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能,据说性能的提升不是一点两点(没有亲测)当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。3、获取Value值
serviceNumberCache.get(); get()对应的源码:
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); }
setInitialValue()对应的源码:
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; }
首先获取对应的线程,再调用getMap()方法,获取每个线程自己维护的ThreadLocalMap映射表,如果存在,就以定义的threadLocal作为Key取保存的对应的值,如果不存在,就取默认值.
以上是结合源码对ThreadLocal 相关方法的介绍,接下来分析ThreadLocal会不会出现内存泄漏
2.1、首先看一下 ThreadLocalMap 里面的Entity 的定义,Entity 里面的key是 弱引用了 threadLocal
static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } } } 在threadlocal的生命周期中,都存在这些引用. 看下图: 实线代表强引用,虚线代表弱引用.
就是因为这个弱引用,有人认为ThreadLocal会引发内存泄露,他们的理由是这样的: 如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链: Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄露。
下面我们看一下 ThreadLocal 的set()方法,大体思路就是:
1.先取到一个Entity,
2.判断Entity.key是否是所需要的
3.如果是直接返回
4.如果不是,判断Entity.key 是否为空
private void set(ThreadLocal key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; //这里是将长度和HashCode进行位运算,其实就是对Len取余 int i = key.threadLocalHashCode & (len-1); // 使用线性探测法查找元素 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); // 这里就是清除 key 为 null 的情况 return; } } tab[i] = new Entry(key, value); int sz = ++size; // 当size 大于 阀值的时候,其实还是会再次清除key为null if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } 关于 set 方法,有几点需要地方: ①、 int i = key.threadLocalHashCode & (len-1);这里实际上是对 len 进行了取余操作。之所以能这样取余是因为 len 的值比较特殊,是 2 的 n 次方,减 1 之后低位变为全 1,高位变为全 0。例如 16,减 1 之后对应的二进制为: 00001111,这样其他数字中大于 16 的部分就会被 0 与掉,小于 16 的部分就会保留下来,就相当于取余了。 接着这里每次调用set()时,值 i 都是会变,这一点比较重要,刚开始我也有点迷糊,我当初认为如果真的存在key 为null 的情况,并且每次get()获取Value 的时候,第一次就获取到或者在key 为null 的位置之前获取到,这样不是永远都不会清楚掉key 为null 的情况,这样不就会造成内存溢出,其实是不会的,key.threadLocalHashCode 会一直在变,注释上也说明了,会自动跟新( the next hash code to be given out ,Updated atomically) private final int threadLocalHashCode = nextHashCode(); /** * The next hash code to be given out. Updated atomically. Starts at * zero. */ private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; /** * Returns the next hash code. */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } 从上面源码可以看出,我们每次获取threadLocalHashCode 的值得时候,都是调用 nextHashCode()方法,而 nextHashCode() 方法每次都是在AtomicInteger 变量(初始值为0)的基础上自动加一个 HASH_INCREMENT (0x61c88647),这样每次获取的位置都会在变,而且还是跳跃式变化,只要调用的次数够多,一定能够将key =null 的数据 清除。 接着再说一下 0x61c88647 这个数字,这是一个神奇的数字,它可以使 hashcode 均匀的分布在大小为 2 的 N 次方的数组里,没有一点冲突,十分均匀。下面为测试代码: public static void main(String[] args) { AtomicInteger nextHashCode = new AtomicInteger(); int HASH_INCREMENT = 0x61c88647; int size = 64; List <Integer> list = new ArrayList <Integer> (); for (int i = 0; i < size; i++) { list.add(nextHashCode.getAndAdd(HASH_INCREMENT) & (size - 1)); } System.out.println("排序前:" + list); Collections.sort(list); System.out.println("排序后: " + list); } 分别将size 设置为 16,32,64 测试结果: //size=16 排序前:[0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9] 排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] //size=32 排序前:[0, 7, 14, 21, 28, 3, 10, 17, 24, 31, 6, 13, 20, 27, 2, 9, 16, 23, 30, 5, 12, 19, 26, 1, 8, 15, 22, 29, 4, 11, 18, 25] 排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31] //size=64 排序前:[0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 6, 13, 20, 27, 34, 41, 48, 55, 62, 5, 12, 19, 26, 33, 40, 47, 54, 61, 4, 11, 18, 25, 32, 39, 46, 53, 60, 3, 10, 17, 24, 31, 38, 45, 52, 59, 2, 9, 16, 23, 30, 37, 44, 51, 58, 1, 8, 15, 22, 29, 36, 43, 50, 57] 排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63] 从结果可以看出,真的是均匀分布,对于这个数字,我们暂时不去深究,只能说一句太神奇了. ②、replaceStaleEntry 和 cleanSomeSlots 方法中都会清理一些key 为null的 数据 ③、当size 大于阀值的时候,也是会先清除一些key 为null的 数据,再判断清理后的大于是否大于阀值的3/4,如果仍然大于,进行扩容操作,如果小于便暂时不扩容. 从set()方法可以看出我们已经有了防止内存泄漏的机制,接下来我们再看一下 getEntity()方法是否也做了相应的处理,源码: 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); } 从 getEntity() 方法可以看出,也是先去取一个值是否是所需要的,如果不是,调用getEntryAfterMiss(),相应的源码如下: 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; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; } 可以看到在getEntryAfterMiss() 方法里面,会对取到的Entity 判断,如果是所需要的,直接返回,如果不是,判断key 是否为null,如果为null ,调用 expungeStaleEntry() 将其清除,如果不为null,继续下一次循环,直到Entity 为null. 从上面分析来看,在我们调用get(),set()方法时,都是会不断的检查是否存在 key= null 的情况,如果存在就将其清除. 那么Entry内的value也就没有强引用链,自然会被回收。所有说 只要还在调用get(),set()方法 ,就是不会发生内存溢出的问题.(如果有也最多有一个对象泄漏,即最后一次设置的那个值,因为在以后再也没有调用过任何方法,而且线程一直没有结束,如永远放在线程池里面). 所以为了不让这种情况发生,建议手动调用ThreadLocal的remove函数来释放,接下来我们来看一下 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; } } } public void clear() { this.referent = null; } 从这个源码,我们看到了什么? remove() 方法分为了两步: 1.将 Entry 的键值Key设为 null, 2.调用 expungeStaleEntry 清理陈旧的 Entry。 那些 认为 key 为null 会造成内存泄漏的,看到 remove() 方法就是分为两步,第一步就是 将 键值Key设为 null ,是不是有点崩溃,而我们在调用 get(),set() 方法时,就是在 执行 remove() 里面的第二步。 总结: 1.Thread正常结束,就算 ThreadLocal 设置为null,不会内存泄漏,因为 ThreadLocalMap 是Thread 里面的一个属性,Thread 销毁,ThreadLocalMap 也不再存在. 2. Thread 放在线程池里面,不销毁,ThreadLocal 设置为null,如果还在一直调用 set(),get() 也是不会出现内存泄漏的情况,因为 get(),set() 里面 有 检查 key =null 的机制,如果发现会清除. 3. Thread 放在线程池里面,不销毁,ThreadLocal 设置为null ,如果以后再也不调用set(),get(),remove() 等方法,就是以后再也不用了,那么会出现内存溢出,但最多有一个对象泄漏,即最后一次设置的那个值,因为在以后再也没有调用过任何方法,而且线程一直没有结束,如永远放在线程池里面。 部分总结来自如下文章,他总结得比我精辟太多.参考: http://qifuguang.me/2015/09/02/[Java并发包学习七]解密ThreadLocal/