ThreadLocal 源码设计分析
本文将通过分析 ThreadLocal
源码,了解其背后的程序设计思路。
作用与场景
ThreadLocal 为不同的 Thread 提供了不同的变量副本,常用于变量在线程间隔离的场景。
举个例子
多个工作线程同时请求网络,获取数据后,对数据进行上报、转换、输出日志等操作。因此每个线程要创建一个 Data 对象用以保存、操作数据。
工作线程:
public class AskThread extends Thread {
private Data data;
@Override
public void run() {
super.run();
if (askNet()) {
DataHandle.pull(data); //上报
Log.e(AskThread.class.getSimpleName(), DataHandle.parseString(data)); //转换为字符串
}
}
private boolean askNet() {
// 模拟从网络获取数据 这里省略 用下面代码模拟
data = new Data();
data.setMessage("网络数据");
return true;
}
}
数据 Bean:
public class Data {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String toString() {
return "Data{" +
"message='" + message + '\'' +
'}';
}
}
数据处理者:
public class DataHandle {
public static void pull(Data data) {
// 模拟上报
// 省略...
}
public static String parseString(Data data) {
// 模拟将Data转为字符串信息
return Thread.currentThread().getName() + " " + data.toString();
}
}
这种写法很常见,由于对数据的处理需要依赖工具类 DataHandle
,因此需要将依赖(Data
)注入到 DataHandle
中,这就不可避免的产生耦合。
假如使用 ThreadLocal,则可以规避这个问题:
public class DataHandle {
public static ThreadLocal<Data> sDataLocal = new ThreadLocal<>();
public static void pull() {
Data data = sDataLocal.get();
// 模拟上报
// 省略...
}
public static String parseString() {
// 模拟将Data转为字符串信息
Data data = sDataLocal.get();
if (data != null) {
return Thread.currentThread().getName() + " " + data.toString();
}
return "";
}
}
public class AskThread extends Thread {
@Override
public void run() {
super.run();
if (askNet()) {
DataHandle.pull();//1
Log.e(AskThread.class.getSimpleName(), DataHandle.parseString());//2
}
}
private boolean askNet() {
// 模拟从网络获取数据
Data data = new Data();
data.setMessage("网络数据");
DataHandle.sDataLocal.set(data);
return true;
}
}
注释 1、2 处可以看出,DataHandle
并不需要关注传参,只需要调用就可以了。
修改后的代码不仅逻辑上更加清晰、代码更加简洁,同时消除了依赖注入的耦合。
从上边也可以看出,ThreadLoacl 并不是无可替代,它只是提供了一种优雅的解决方案。
源码分析
ThreadLoacl
提供了线程的变量副本。
因此如果我们自己实现,一般会通过 Map 保存 Thread 对应的变量副本。代码如下:
public void set(Object value){
map.put(Thread.currentThread(), value);
}
public void get(){
map.get(Thread.currentThread());
}
它的层级关系如下:
层级关系1这样实现有俩个问题,先说第一个:
首先,多线程操作同一个 Map,必然涉及到同步,虽然这个问题可以通过加锁轻松解决,但需要注意的是,我们设计 ThreadLocal 的初衷是创建一个线程变量副本的提供者,它不负责、也不参与任何线程共享逻辑,因此这样设计显然违背了初衷。
上述是以 ThreadLocal 为出发点,Thread 为 Key,保存变量副本。
如果我们转换一下思路,以 Thread 为出发点,ThreadLocal 为 Key 来保存变量副本呢?ThreadLocal 源码就是这样做的。来看下 set 源码:
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);
}
层级结构因此转变为:
层级关系2每个线程都创建一个 map 来保存变量副本(也就是说 map 由 thread 本身来维护)。其中 Key 为 ThreadLocal 对象本身,Value 为保存值。
这样,当线程获取变量副本时,使用的 Map 是线程自己的,因此没有多线程共享同步问题。
俩者表达的意义也是不同的:
前者重心在 ThreadLocal 本身保存了多个线程的变量副本;
后者重心在 Thread 通过 ThreadLocal 保存了当前线程的变量副本。
我们的实现方式还有第二个问题:
除非在线程结束时手动从 map 中 remove 掉 key,否则会导致 Thread 内存泄漏。
靠手动维护的代码难以保证安全性,ThreadLocal 是怎么做的呢?
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
...
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
...
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
threadlocal 中,Map 的 Key 是弱引用,即 threadlocal 是弱引用,因此当把 threadlocal 实例置为 null 以后,没有任何强引用指向 threadlocal 实例,所以 threadlocal 将会被正常回收。即 key 不存在内存泄漏问题。
但是我们的 value 是强引用,它能正常回收吗?
我们来分析下场景。
threadlocal 置为 null,此时 value 由于与运行中线程存在强引用关系,故 value 不能回收。
场景 1:
运行线程结束,此时 value 唯一强引用断开,GC 正常回收;
场景 2:
线程运行时间长、或属于线程池复用线程。此时因为线程无法销毁、value 无法销毁,导致内存泄漏。
再次强调 map 是 thread 来维护的,所以 value 无法回收不会导致 threadlocal 内存泄漏。
但是联系实际,就会发现内存泄漏其实很难出现(或者出现时间很短)。
原因是因为一般情况下不会单独将 threadlocal 置为 null(置为 null 就说明调用它的线程不再需要这个变量副本);即使置为 null,在运行线程关闭后 value 也会正常回收;纵使在极端例子中,如上述不回收 value 的复用线程里,threadlocal 在 set
、get
、remove
等方法里会遍历所有 key == null
的键值对,并进行了删除 value 的处理,所以开发者也可以及时的调用 remove
以避免内存泄漏。
总结
ThreadLocal
的源码设计非常巧妙,不应该仅仅会用,更应该熟悉其源码背后的设计思路。
本人能力有限,有问题欢迎及时指正。