接下来笔者的文章方向偏向于 Android & Java 面试相关知识点系统性的总结,欢迎关注。
ThreadLocal类是java.lang包下的一个类,用于线程内部的数据存储,通过它可以在指定的线程中存储数据,本文针对该类进行原理分析。
通过思维导图对其进行简单的总结:
一.ThreadLocal源码分析
ThreadLocal类最重要的几个方法如下:
- get():T 获取当前线程下存储的变量副本
- set(T):void 存储该线程下的某个变量副本
- remove():void 移除该线程下的某个变量副本
1.get()方法分析
ThreadLocal类比较简单,其最重要的就是get()和set()方法,顾名思义,起作用就是取值和设置值:
// 获取当前线程中的变量副本
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程中的ThreadLocalMap对象
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;
}
}
// 若没有该变量副本,返回setInitialValue()
return setInitialValue();
}
这里先将ThreadLocalMap暂时理解为一个Map结构的容器,内部存储着该线程作用域下的的所有变量副本,我们从ThreadLocal类中取值的时候,实际上是从ThreadLocalMap中取值。
如果Map中没有该变量的副本,会从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;
}
可以看到,setInitialValue()中也非常的简单,依然是从当前线程中获取到ThreadLocalMap,略微不同的是,setInitialValue()会对变量进行初始化,存入ThreadLocalMap中并返回。
这个初始化的方法的执行,需要开发者自己重写initialValue()方法,否则返回值依然为null。
public class ThreadLocal<T> {
// ...
protected T initialValue() {
return null;
}
}
2.set()方法分析
和setInitialValue()方法类似,set()方法也非常简单:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// map不为空,直接将ThreadLocal对象作为key
// 变量本身的值为value,存入map
map.set(this, value);
else
// 否则,创建ThreadLocalMap
createMap(t, value);
}
可以看到,这个方法的作用就是将变量副本作为value存入Map,需要注意的是,key并非是我们下意识认为的Thread对象,而是ThreadLocal本身(Thread和Value本身是一对一的,我们更容易将其映射为key-value的关系)。
3.remove()方法分析
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
对于变量副本的移除,也是通过map进行处理的,和set()和get()相同,Entry的键值对中,ThreadLocal本身作为key,对变量副本进行检索。
4.小结
可以看出,ThreadLocal本身内部的逻辑都是围绕着ThreadLocalMap在运作,其本身更像是一个空壳,仅作为API供开发者调用,内部逻辑都委托给了ThreadLocalMap。
接下来我们来探究一下ThreadLocalMap和Thread以及ThreadLocal之间的关系。
二、ThreadLocalMap分析
ThreadLocalMap内部代码和算法相对复杂,个人亦是一知半解,因此就不逐行代码进行分析,仅系统性进行概述。
首先来看一下ThreadLocalMap的定义:
public class ThreadLocal<T> {
// ThreadLocalMap是ThreadLocal的内部类
static class ThreadLocalMap {
// Entry类,内部key对应的是ThreadLocal的弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
// 变量的副本,强引用
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
ThreadLocal中的嵌套内部类ThreadLocalMap本质上是一个map,依然是key-value的形式,其中有一个内部类Entry,其中key可以看做是ThreadLocal实例的弱引用。
和最初的设想不同的是,ThreadLocalMap中key并非是线程的实例Thread,而是ThreadLocal,那么ThreadLocalMap是如何保证同一个Thread中,ThreadLocal的指定变量唯一呢?
// 1.ThreadLocal的set()方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// ...
}
// 2.getMap()实际上是从Thread中获取threadLocals成员
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public class Thread implements Runnable {
// 3.每个Thread实例都持有一个ThreadLocalMap的属性
ThreadLocal.ThreadLocalMap threadLocals = null;
}
Thread本身持有ThreadLocal.ThreadLocalMap的属性,每个线程在向ThreadLocal里setValue的时候,其实都是向自己的ThreadLocalMap成员中加入数据;get()同理。
三、内存泄漏的风险?
在上一小节中,我们看到ThreadLocalMap中的Entry中,其ThreadLocal作为key,是作为弱引用进行存储的。
当ThreadLocal不再被作为强引用持有时,会被GC回收,这时ThreadLocalMap对应的ThreadLocal就变成了null。而根据文档所叙述的,当key == null时,这时就可以默认该键不再被引用,该Entry就可以被直接清除,该清除行为会在Entry本身的set()/get()/remove()中被调用,这样就能 一定情况下避免内存泄漏。
这时就有一个问题出现了,作为key的ThreadLocal变成了null,那么作为value的变量可是强引用呀,这不就导致内存泄漏了吗?
其实一般情况下也不会,因为即使再不济,线程在执行结束时,自然也会消除其对value的引用,使得Value能够被GC回收。
当然,在某种情况下(比如使用了 线程池),线程再次被使用,Value这时依然可以被获取到,自然也就发生了内存泄漏,因此此时,我们还是需要通过手动将value的值设置为null(即调用ThreadLocal.remove()方法)以规避内存泄漏的风险。
参考&感谢
- 《Android开发艺术探索》
- 深入理解ThreadLocal的"内存溢出"
- 关于ThreadLocal内存泄露的备忘
- ThreadLocal源码分析
关于我
Hello,我是却把清梅嗅,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的博客或者Github。
如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?