ThreadLocal原理以及为什么会出现内存泄漏
一、ThreadLocal
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLocal类主要有四个方法,分别是:
1)ThreadLocal.get:用来获取ThreadLocal在当前线程中保存的变量副本
2)ThreadLocal.set:用来设置ThreadLocal在当前线程中变量的副本
3)ThreadLocal.remove:用来删除ThreadLocal在当前线程中变量的副本
4)ThreadLocal.initialValue:是一个protected方法,一般是用来在使用时进行重写的。在调用get()方法时,如果ThreadLocal没有被当前线程赋值或当前线程刚调用remove方法,就返回此方法值。
1.ThreadLocal的使用
public class ThreadLocalTest {
public static ThreadLocal<String> threadLocalVar = new ThreadLocal<String>() {
@Override
public String initialValue() {
Thread t = Thread.currentThread();
System.out.println(t.getName() + "调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!");
return "hello";
}
};
public static void main(String[] args) {
new Thread(new SimpleThread("beijing")).start();
new Thread(new SimpleThread("北京")).start();
}
}
class SimpleThread implements Runnable {
private String value;
public SimpleThread(String value) {
this.value = value;
}
public void run() {
Thread t = Thread.currentThread();
String value1 = ThreadLocalTest.threadLocalVar.get();
System.out.println("threadName=" + t.getName() + ";最初value1=" + value1);
ThreadLocalTest.threadLocalVar.set(value);
String value2 = ThreadLocalTest.threadLocalVar.get();
System.out.println("threadName=" + t.getName() + ";设值后value2=" + value2);
ThreadLocalTest.threadLocalVar.remove();
String value3 = ThreadLocalTest.threadLocalVar.get();
System.out.println("threadName=" + t.getName() + ";删除后value3=" + value3);
}
}
运行结果:
Thread-0调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!
Thread-1调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!
threadName=Thread-0;最初value1=hello
threadName=Thread-0;设值后value2=beijing
threadName=Thread-1;最初value1=hello
threadName=Thread-1;设值后value2=北京
Thread-1调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!
threadName=Thread-1;删除后value3=hello
Thread-0调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!
threadName=Thread-0;删除后value3=hello
二、ThreadLoal原理
1.ThreadLocal.get
/**
* 返回该thread-local变量在当前线程中的值
* 如果当前线程中没有该hread-local变量的值,则该变量值被初始化成调用initialValue()方法后返回的值
* @return
*/
public T get() {
Thread t = Thread.currentThread();//返回当前正在执行的线程的引用
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);//注意此时的“this”是指该thread-local变量
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//当前线程中没有该thread-local变量的值
return setInitialValue();
}
//返回该线程Thread内部的一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals
ThreadLocal.ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
1)如果当前线程中没有该thread-local变量的值
//设值初始值
private T setInitialValue() {
/**
* initialValue方法默认情况下返回的是null
* 注意如果不重写initialValue()方法,在get之前不调用set也能正常运行,不会出现空指针异常,
* 只不过该thread-local变量在当前线程中的值为null而已
*/
T value = initialValue();
java.lang.Thread t = java.lang.Thread.currentThread();
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//注意此时的“this”是指该thread-local变量
else
createMap(t, value);
return value;
}
i)如果ThreadLocal.ThreadLocalMap map = getMap(t);中 map为null
void createMap(java.lang.Thread t, T firstValue) {
t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);//注意此时的“this”是指该thread-local变量
}
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;
}
}
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
从代码中,可以看到ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值。
ii)如果ThreadLocal.ThreadLocalMap map = getMap(t);中 map不为null
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);//计算index索引位置
//从i位置开始,依次加1直至找到没有被其他key值的元素占用的位置或找到自己相同key的位置,并替换掉之前的值
for (ThreadLocal.ThreadLocalMap.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);
return;
}
}
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
从上面的代码可以看出:ThreadLocal中hash冲突的解决方法:
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方法就是简单的步长加1,寻找下一个相邻的位置。
2)如果当前线程中有该thread-local变量的值
//以当前thread-local变量值作为key,从ThreadLocalMap中获取对应的Entry节点
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)//没有hash冲突,直接返回对应index位置上的值
return e;
else
return getEntryAfterMiss(key, i, e);
}
//有hash冲突的情况下,以当前thread-local变量值作为key,从ThreadLocalMap中获取对应的Entry节点
private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) {
ThreadLocal.ThreadLocalMap.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;
}
对get方法进行总结:
在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
2.ThreadLocal.set
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
set方法中的代码,我们在get方法中已经分析过了
3.ThreadLocal.remove
public void remove() {
ThreadLocal.ThreadLocalMap m = getMap(java.lang.Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//只所以用for循环查找key,是因为要考虑hash冲突的情况
for (ThreadLocal.ThreadLocalMap.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;
}
三、ThreadLocal为什么会出现内存泄漏
ThreadLocal的核心机制:
每个Thread内部维护着一个ThreadLocalMap,它是一个Map。这个映射表的Key是一个弱引用,其实就是ThreadLocal本身,Value是真正存的线程变量Object。
也就是说ThreadLocal本身并不真正存储线程的变量值,它只是一个工具,用来维护Thread内部的Map,帮助存和取。所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
1.ThreadLocal为什么会出现内存泄漏
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal 本身并不存储值,它只是作为一个 key保存到ThreadLocalMap中,但是这里要注意的是它作为一个key用的是弱引用,因为没有强引用链,弱引用在GC的时候可能会被回收。这样就会在ThreadLocalMap中存在一些key为null的键值对(Entry)。因为key变成null了,我们是没法访问这些Entry的,但是这些Entry本身是不会被清除的,为什么呢?因为存在一条强引用链。即线程本身->ThreadLocalMap->Entry也就是说,恰恰我们在使用线程池的时候,线程使用完了是会放回到线程池循环使用的。由于ThreadLocalMap的生命周期和线程一样长,如果没有手动删除对应key就会导致这块内存即不会回收也无法访问,也就是内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。但是这些举动不能保证内存就一定会回收,因为可能这条线程被放回到线程池里后再也没有使用,或者使用的时候没有调用其get(),set(),remove()方法。
内存泄漏归根结底是由于ThreadLocalMap的生命周期跟Thread一样长
1)如何避免内存泄漏
调用ThreadLocal的get()、set()方法后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
在不使用线程池的前提下,即使不调用remove方法,线程的"变量副本"也会被gc回收,即不会造成内存泄漏的情况。