一、ThreadLocal 使用
ThreadLocal 是一个关于创建线程局部变量的类。通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用 ThreadLocal 建的变量只能被当前线程访问,其他线程则无法访问和修改。接下来我们 ThreadLocal 如何使用:
创建,支持泛型:
ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>();
set方法:
mStringThreadLocal.set("name");
get方法:
mStringThreadLocal.get();
完整的使用示例:
private void testThreadLocal() {
Thread t = new Thread() {
ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>();
@Override
public void run() {
super.run();
mStringThreadLocal.set("name");
mStringThreadLocal.get();
}
};
t.start();
}
为 ThreadLocal 设置默认的 get 初始值,需要重写 initialValue 方法,下面是一段代码,我们将默认值修改成了线程的名字:
ThreadLocal<String> mThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return Thread.currentThread().getName();
}
};
在 Android 中的应用,Looper 类就是利用了 ThreadLocal 的特性,保证每个线程只存在一个 Looper 对象:
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
二、ThreadLocal 原理
为了更好的掌握 ThreadLocal,了解其内部实现是很有必要的,这里我们以 set 方法起始看一看 ThreadLocal 的实现原理。下面是 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);
}
大致意思为首先获取当前线程,利用当前线程作为句柄获取一个 ThreadLocalMap 的对象,如果上述 ThreadLocalMap 对象不为空,则设置值,否则创建这个 ThreadLocalMap 对象并设置值。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
上面是一个利用 Thread 对象作为句柄获取 ThreadLocalMap 对象的代码。它获取的实际上是 Thread 对象的 threadLocals 变量,可参考下面代码:
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
而如果一开始设置,即 ThreadLocalMap 对象未创建,则新建 ThreadLocalMap 对象,并设置初始值。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
实际上 ThreadLocal 的值是放入了当前线程的一个 ThreadLocalMap 实例中,所以只能在本线程中访问,其他线程无法访问。
ThreadLocalMap 是什么呢?从名字上看,可以猜到它也是一个类似HashMap的数据结构,但是在ThreadLocal中,并没实现Map接口。在 ThreadLoalMap 中,也是初始化一个大小 16 的 Entry 数组,Entry 对象用来保存每一个 key-value 键值对,只不过这里的 key 永远都是 ThreadLocal 对象,是不是很神奇,通过 ThreadLocal 对象的 set 方法,结果把 ThreadLocal 对象自己当做 key,放进了 ThreadLoalMap 中。value 则是要存储的对象。这里需要注意的是,ThreadLoalMap 的 Entry 是继承 WeakReference,和 HashMap 很大的区别是,Entry 中没有 next 字段,所以就不存在链表的情况了。
有了上面的基础,我们看 get() 方法就一点都不难理解了:
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();
}
对象存放在哪里呢?
在 Java 中,栈内存归属于线程私有,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。而堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。那么是不是说 ThreadLocal 的实例以及其值存放在栈上呢?其实不是,因为 ThreadLocal 实例实际上也是被其创建的类持有(更顶端应该是被线程持有)。而 ThreadLocal 的值其实也是被线程实例持有。它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
三、会导致内存泄露么
为什么 ThreadLocal 可能导致内存泄漏?先看看 Entry 的实现:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
通过之前的分析已经知道,当使用 ThreadLocal 保存一个 value 时,会在 ThreadLocalMap 中的数组插入一个Entry对象,按理说 key-value 都应该以强引用保存在 Entry 对象中,但在 ThreadLocalMap 的实现中,key 被保存到了 WeakReference 对象中。这就导致了一个问题,ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,如果创建 ThreadLocal 的线程一直持续运行,那么这个 Entry 对象中的 value 就有可能一直得不到回收,发生内存泄露。
既然已经发现有内存泄露的隐患,自然有应对的策略,在调用 ThreadLocal 的 get()、set() 可能会清除 ThreadLocalMap 中 key 为 null 的 Entry 对象,这样对应的 value 就没有 GC Roots 可达了,下次 GC 的时候就可以被回收,当然如果调用 remove 方法,肯定会删除对应的 Entry 对象。如果使用 ThreadLocal 的 set 方法之后,没有显示的调用 remove 方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完 ThreadLocal 之后,记得调用 remove 方法。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("android");
// 其它业务逻辑
} finally {
localName.remove();
}