ThreadLocal的使用原理及内存泄漏原因
ThreadLocal类,字面意思是本地变量,ThreadLocal可以为在每个线程中都创建一个副本,每个线程可以访问自己内部的副本变量。
1.API使用
简单看看如何使用这个类:
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal<String> name = new ThreadLocal<>();
new Thread(()->{
name.set("ABin");
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+": "+name.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程1").start();
new Thread(()->{
name.set("Someone");
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+": "+name.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程2").start();
}
}
输出结果:
线程1: ABin
线程2: Someone
从结果可以看出,线程1和线程2都修改了name的值,但是无论我们运行多少次,都不会出现线程同步的问题,因为两个线程内都有一个name变量的副本,各自修改互不影响。
2.线程中副本变量如何实现的
从上面的例子可以看出,ThreadLocal通过set
、get
方法,来设置、读取线程中的本地变量,那么先看这两个方法:
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前运行线程
ThreadLocalMap map = getMap(t); // 获得当前运行线程的ThreadLocalMap
if (map != null)
map.set(this, value); // 如果不为null,则放入此map
else
createMap(t, value); // 如果为空则为此线程创建map
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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();
}
从这段代码可以看出,每个线程在调用set
方法的时候,都会向线程中的ThreadLocalMap变量中放入ThreadLocal的引用
和value
。而每次调用get
方法的时候,都是从线程中的ThreadLocalMap变量中取值。
那么看看ThreadLocalMap类做了什么:
// 这里只摘录了重要的代码,详细的可以去看源码
// 首先可以看到 ThreadLocalMap 是 ThreadLocal 的一个静态内部类
static class ThreadLocalMap {
// 这里使用了弱引用,作用就是当一个变量没有被强引用,且JVM发起GC的时候,会回收被弱引用的变量的空间
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
// get()时会调用此方法,返回Map中对应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); // 如果不存在这个Entry,或者这个Entry的键是null,就会走这个方法。
}
// 这个方法的作用:1.如果Entry是null,那么返回null,也就是不存在这个ThreadLocal变量。2.如果Entry的键为null,那么走expungeStaleEntry方法,清理掉所有键为null的Entry
// 什么时候Entry的键为null?没有变量强引用ThreadLocal变量,且JVM内存不够发起一次GC后
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;
}
// 这个方法会将键为空的Entry的value和Entry都置为null,方便GC
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
// 向Map中存放一个ThreadLocal和value
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// 从map中移除一个ThreadLocal变量,同时会清理掉所有键为null的Entry
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;
}
}
}
}
总结一下:每个线程中都有一个ThreadLocalMap变量,这个变量可以存放多个ThreadLocal变量及其值,当某一个线程使用ThreadLocal实例的set()、get()
时候,会向这个线程的Map中存、取。
3.为什么Entry使用弱引用
首先明确,Java中引用传递的是堆中的地址,那么如果ThreadLocal被置为null或指向其他实例的时候,JVM理应回收这部分空间(因为ThreadLocal实例改变了,原来内存中的值无法通过get()得到了)。但是如果Entry采用强引用,并且线程迟迟不结束(常见的是线程池),那么就会导致这部分空间明明没有用但无法清理,也就是内存泄漏产生的一个原因。
4.内存泄漏
虽然采用了虚引用,但还是存在内存泄漏的可能:Entry只虚引用了ThreadLocal,而value则是被Entry强引用的。如果ThreadLocal指向发生改变,GC的时候会回收ThreadLocal的空间,并不会回收value的空间,**可以通过remove()**来安全的移除。
// ThreadLocal
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
// ThreadLocalMap
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); // 会将value置为null,方便GC
return;
}
}
}
所以JDK建议将ThreadLocal变量定义成private static
的,这样ThreadLocal的生命周期更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
此外,上面也提到过ThreadLocalMap的getEntry()或者set(),会清理掉Map中键为null的记录。
5.为什么不把value设为虚引用
因为如果 value实例被释放了,但ThreadLocal中的value还有可能需要继续使用。
public class ThreadLocalTest {
static ThreadLocal<String> name = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(()->{
name.set("ABin"); // value引用的字符串常量池中的值
}).start();
}
}