在上一篇(看懂ThreadLocal,有这一篇就够了)文章中,我们聊了聊ThreadLocal的基本原理。今天,我们再对ThreadLocal更高级的知识进行学习,主要包含两部分内容:
1、InheritableThreadLocal
2、ThreadLocal内存泄露问题
1.InheritableThreadLocal
首先,我们先看一个例子:
public class ThreadLocalDemo {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("main Thread");
Thread thread = new Thread(() -> {
System.out.println("子线程value:" + threadLocal.get());
});
thread.start();
System.out.println("主线程value:" + threadLocal.get());
}
}
上面代码在主线程中设置了threadLocal变量,并将其值设置为“main Thread”;然后启动了一个子线程thread, 并试图在子线程中获取在主线程中设置的threadLocal变量的值。但是结果如下:
主线程value:main Thread
子线程value:null
通过上一篇文章的介绍,对上面的结果我们也就非常容易理解了。
每个线程都有自己单独的ThreadLocalMap,所以子线程在调用threadLocal.get()方法时访问的是自己的ThreadLocalMap, 这个ThreadLocalMap和主线程的ThreadLocalMap是两个map, 所以子线程肯定是获取不到主线程设置的ThreadLocal变量的值。
那有没有办法能够让子线程访问到主线程设置的threadLocal变量的值呢?
答案就是用InheritableThreadLocal。
将上面的代码稍微做一点改变,如下所示:
public class ThreadLocalDemo {
private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("main Thread");
Thread thread = new Thread(() -> {
System.out.println("子线程value:" + threadLocal.get());
});
thread.start();
System.out.println("主线程value:" + threadLocal.get());
}
}
上面代码,我们将ThreadLocal替换为InheritableThreadLocal,其他一切都未变。运行结果如下:
主线程value:main Thread
子线程value:main Thread
可以看到这次在子线程能够访问到父线程设置在InheritableThreadLocal变量中的值。
那我们不禁要问:InheritableThreadLocal是如何做到这一切的呢?
让我们进入进入get()方法一探究竟:
public class ThreadLocal<T> {
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();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
}
我们看到InheritableThreadLocal.get()方法就是调用了父类ThreadLocal.get()方法。InheritableThreadLocal继承了ThreadLocal类,但是InheritableThreadLocal重写了父类的getMap()方法, InheritableThreadLocal源代码如下:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
在父类ThreadLocal的get()方法返回的是线程t的threadLocals成员变量, 而在子类InheritableThreadLocal的get()方法返回的是线程t的inheritableThreadLocals成员变量。
在Thread类中,我们也确实看到有这两个成员变量:
class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
就是这一点不同,导致了上面两个示例代码得到不同的结果。
也就是说,当我们使用InheritableThreadLocals.get()时,是从线程t的成员变量inheritableThreadLocals中获取对应的变量值,当使用ThreadLocal.get()时,是从线程t的成员变量threadLocals中获取对应变量的值。
进一步推出:子线程中的inheritableThreadLocals获取到了了父线程的inheritableThreadLocals中变量的值,而子线程中的threadLocals却获取不到父线程的threadLocals中变量的值。
那父线程是什么时候将自己成员变量inheritableThreadLocals中保存的变量值传递给了子线程的成员变量inheritableThreadLocals呢?
我们看下new Thread()这行代码发生了什么:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// 注意区分:currentThread()获取到的是主线程,因为new Thread()方法是在主线程中执行的
Thread parent = currentThread();
....忽略其他代码
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
通过上面代码,我们清楚地看到,在主线程中调用new Thread()方法去创建子线程时,在内部会将主线程的inheritableThreadLocals成员变量中保存的值传递给子线程的成员变量inheritableThreadLocals。
这就是为什么使用InheritableThreadLocal, 能够使得子线程访问到父线程的InheritableThreadLocal设置的值。
还有一点需要注意:
一旦子线程被创建以后,再操作父线程中的InheritableThreadLocal变量,那么子线程是不能感知的。因为父线程和子线程还是拥有各自的inheritableThreadLocals成员变量,只是在创建子线程的“一刹那”将父线程的inheritableThreadLocals复制给子线程,后续两者就没啥关系了。
通过以下代码,我们可以验证上面的结论:
public class ThreadLocalDemo {
private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("main Thread");
Thread thread = new Thread(() -> {
System.out.println("子线程value:" + threadLocal.get());
});
threadLocal.set("main Thread update");
thread.start();
System.out.println("主线程value:" + threadLocal.get());
}
}
上面代码在主线程创建完子线程后,更改了主线程InheritableThreadLocal中保存的值。
运行结果如下:
主线程value:main Thread update
子线程value:main Thread
我们看到,确实在创建完子线程后,再操作父线程中的InheritableThreadLocal变量,那么子线程是不能感知的。
2.ThreadLocal内存泄露问题
到目前为止,我们基本上对ThreadLocal相关知识都掌握得差不多了。但是有一个点,我们一直没讲到。
看过ThreadLocal源码的人都应该知道,ThreadLocalMap是以ThreadLocal的弱引用作为key,那你有没有想过设计者为什么要把ThreadLocal的弱引用作为key呢?
这就涉及到的弱引用的作用和内存泄露问题。
java中的对象引用分为强引用、弱引用、软引用、虚引用。对对象进行弱引用不会影响垃圾回收器回收该对象,即如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。
再来说说内存泄漏,假如一个短生命周期的对象被一个长生命周期对象长期持有引用,将会导致该短生命周期对象使用完之后得不到释放,从而导致内存泄漏。
因此,弱引用的作用就体现出来了,可以使用弱引用来引用短生命周期对象,这样不会对垃圾回收器回收它造成影响,从而防止内存泄漏。
再回到上面这个问题上来:
如果使用强引用,当ThreadLocal不再使用需要回收时,由于发现某个线程中的ThreadLocalMap存在该ThreadLocal的强引用,从而使得GC无法回收,导致内存泄露。
因此,使用弱引用可以防止长期存在的线程(通常使用了线程池)导致ThreadLocal无法回收造成内存泄漏。
既然通过ThreadLocal的弱引用作为key, 可以避免内存泄露,那通常说的ThreadLocal的内存泄露是怎么回事呢?
我们看下ThreadLocalMap中的Entry类:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
我们看到虽然Entry的key(threadLocal)是弱引用,但是value本身是强引用。
这就导致,假如不作任何处理,由于ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经回收掉了,但value还是驻留在线程的ThreadLocalMap的Entry中。即存在key为null,但value却有值的无效Entry,导致内存泄漏。
其实ThreadLocal已经帮我们尽量避免了内存泄露,在我们每次调用threadLocal的get()、set()、remove()方法时,threadLocal都会将key为null的Entry清除掉,从而避免了内存泄露。具体代码这里就不贴了。
但是该工作是有触发条件的,需要调用相应方法,假如我们使用完之后不做任何处理是不会触发的。这就引出了ThreadLocal的最佳实践。
在上一篇文章中,我也说了使用完ThreadLocal变量要及时调用ThreadLocal.remove()方法清理掉保存的变量,现在看来一方面能够防止信息错乱(由于线程复用),另一方面也能够避免出现内存泄露的问题。
好了,ThreadLocal就讲到这了,希望各位能从中有所收获,没有浪费各位的时间:)
搜索并关注公众号「聊谈说侃」,获取更多精彩文章,我们一起共同进步…