1 ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。
2 ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定。
3 ThreadLocal本身不存储任何值,存在当前线程的ThreadLocalMap中
4 类似 Map 的结构存储变量。每个Thread里面都有一个ThreadLocal.ThreadLocalMap成员变量,也就是说每个线程通过ThreadLocal.ThreadLocalMap与ThreadLocal相绑定。**
1 ThreadLocal
//获取值
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();
}
//初始化
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;
}
//绑定map到当前线程
//设置值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//获取线程关联的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//创建ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
1.1 直接调用,没有set,返回值
返回空,如果重写了 intValue(),则返回这个对象。
2ThreadLocalMap
ThreadLocal.ThreadLocalMap内部类,维护着每个线程自己的多个ThreadLocal变量, key为当前的threadLocal实例(ThreadLocal本身不存值,只是引用),value为设置的值 (值存放在ThreadLocal.ThreadLocalMap.Entry实例中)。关键定义如下:
static class ThreadLocalMap {
//存储数据的数组
private Entry[] table;
//获取Entry,key的类型为ThreadLocal
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);
}
}
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
Entry类是ThreadLocalMap的静态内部类,用于存储数据。key为ThreadLocal本身的弱引用,值为set进来的值。
static class Entry extends WeakReference<ThreadLocal<?>> {
/ The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry类继承了WeakReference
3 内存泄漏的原因
1 Entry的key是个WeakReference弱引用的ThreadLocal对象,会被垃圾回收,回收后,key会变为null,但value还存在,而又无法通过已经变为null的key索引到value,因此value所在内存便无法使用,又无法回收,导致内存泄露。
2 ThreadLocalMap(即位于Thread中的变量)threadLocals本身的生命周期与线程一致,即使ThreadLocal本身弱引用已经回收,但value还存在于ThreadLocalMap中的Entry中,导致内存泄露。
4 如果绑定了ThreadLocal的线程,提交到线程池?
线程,一般不会消亡,ThreadLocalMap,相当一个全局变量,ThreadLocal则会被反复创建,导致内存泄漏。
5 如何避免泄漏(被问到过)
1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
2、JDK建议ThreadLocal定义为private static(只有一个实例不会反复创建对象),这样ThreadLocal的弱引用问题则不存在了。
3 get()、set() remove 都会删除key为null的value;
6ThreadLocalMap 为什么采用开放定址法
闭散列:(开放地址法或者也叫线性探测法)解决哈希冲突。开放地址法有个非常关键的特征,就是所有输入的元素全部存放在哈希表里,哈希表的装载因子不会超过1。它的实现是在插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。
7 get()、set() remove()都会清除?(被问到过)
7.1 set()
1、获取key的hash对应槽位
2、判断当前槽位是否已被占用,若已被占用,则判断key值是否相同,是则直接替换;否则则判断槽位中的
Entry的key值是否为null,为null则走**replaceStaleEntry**方法;
key不相同或不为null则一直往下找,直到遇到空槽位或key相同或key值为null的槽位
3、若直接找到为空的槽位,则放入并清空部分槽位并判断是否需要扩容
7.2 get()
get,直接定位到ThreadLocalMap.getEntry,直接上源码
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e; — 没有冲突时直接返回,没有清entry
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {<br /> ThreadLocal<?> k = e.get();<br /> if (k == key)<br /> return e;<br /> if (k == null)<br /> expungeStaleEntry(i); -- 同上,清理i两边空槽之间的key为null的值,<br /> 如果有两个及以上的key为null的entry,则调用cleanSomeSlots<br /> else<br /> i = nextIndex(i, len);<br /> e = tab[i];<br /> }<br /> return null;<br />}<br /> <br />-- 同上,清理i两边空槽之间的key为null的值,<br /> 如果有两个及以上的key为null的entry,则调用cleanSomeSlots
7.3 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(); — 找到key并将entry.key设为null
expungeStaleEntry(i); — ,
如果有两个及以上的key为null的entry,则调用cleanSomeSlots
return;
}
}
}
说明:不能保证全部清除,但会清除当前key
7.4 总结
总结:除了map扩容时会遍历整个数组进行清除外,其他方法都不能保证全部清除掉所有key为null的entry,除非线程本身被垃圾收集器回收,但现在用的最多的还是线程池,虽然大部分entry和value会被清理,但还会有部分一直存在内存中,所以也不能杜绝内存泄露,最好还是用完后手动remove为好。
8 总结
每个 Thread 里都含有一个 ThreadLocalMap 的成员变量,这种机制将 ThreadLocal 和线程巧妙地绑定在了一起,即可以保证无用的 ThreadLocal 被及时回收,不会造成内存泄露,又可以提升性能。假如我们把 ThreadLocalMap 做成一个 Map
什么时候无用的 Entry 会被清理:
1 Thread 结束的时候
2 插入元素时,发现 staled entry,则会进行替换并清理
3 插入元素时,ThreadLocalMap 的 size 达到 threshold,并且没有任何 staled entries 的时候,会调用 rehash 方法清理并扩容
4 调用 ThreadLocalMap 的 remove 方法或set(null) 。
尽管不会造成内存泄露,但是可以看到无用的 Entry 只会在以上四种情况下才会被清理,这就可能导致一些 Entry 虽然无用但还占内存的情况。因此,我们在使用完 ThreadLocal 后一定要remove一下,保证及时回收掉无用的 Entry。
特别地,当应用线程池的时候,由于线程池的线程一般会复用,Thread 不结束,这时候用完更需要 remove 了。
1 那key为什么不使用强引用?
如果key使用强引用,即使调用threadLocalA = null,此时线程中threadLocalMap中仍然持有threadLocal实例的引用,threadLocalA实例仍 然不会被GC回收,造成异常情况
2 value为什么不使用弱引用?
value只存在thread引用->堆区thread实例->threadLocalMap->entryTable->entry->value这一条引用链,假设value为弱引用,则GC后会被回收,再也无法通过ThreadLocal.get()方法获取value值
总的来说,对于多线程资源共享的问题,同步机制采用了 以时间换空间 的方式,而 ThreadLocal 则采用了 以空间换时间 的方式。前者仅提供一份变量,让不同的线程排队访问;而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。