1、首先我们定义一个对象,并重写了initialValue()方法来设置ThreadLocal的初始值:
private static ThreadLocal serviceNumberCache = new ThreadLocal() {
@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。
这样设计的主要有以下几点优势:
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 {
/** 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
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%E5%B9%B6%E5%8F%91%E5%8C%85%E5%AD%A6%E4%B9%A0%E4%B8%83]%E8%A7%A3%E5%AF%86ThreadLocal/