ThreadLocal 是 Java 提供的一个机制,但在 Android 开发,尤其是在理解 Looper 和性能优化时,它扮演着至关重要的角色。
0x00. ThreadLocal 的核心作用和设计目标
核心作用:线程局部变量
ThreadLocal 顾名思义,是线程的局部变量。它为每个使用该变量的线程都提供了一个独立的、隔离的变量副本。
- 隔离性: 当多个线程访问同一个
ThreadLocal实例时,每个线程看到和操作的都是自己的变量副本,彼此之间互不干扰。 - 线程安全: 由于每个线程都有自己的副本,因此天然解决了多线程访问共享变量时的同步问题,无需使用
synchronized等锁机制,从而提升了并发性能。
注意:
ThreadLocal本身不存储数据,它只是一个访问键(Key)。数据存储在每个线程内部的一个名为ThreadLocalMap的结构中。
0x01. ThreadLocal 的底层原理:ThreadLocalMap
要理解 ThreadLocal 的机制,关键在于理解 Thread 对象内部的一个隐秘字段:
-
数据存储在哪?
- 每个
java.lang.Thread对象内部都有一个特殊的成员变量,叫做ThreadLocal.ThreadLocalMap(注意,这个 Map 是ThreadLocal的内部类,但它属于Thread实例)。 - 当你调用
ThreadLocal.set(value)时,数据实际上是以(thisThreadLocalInstance, value)的形式存入了当前线程的ThreadLocalMap中。
- 每个
-
ThreadLocalMap的结构:ThreadLocalMap并不是一个普通的HashMap。它的键(Key)是**弱引用(Weak Reference)**指向ThreadLocal对象本身,值(Value)是你要存储的数据。
0x02. ThreadLocal 在 Android 源码中的应用
ThreadLocal 最著名的应用就是实现 Android 的消息循环机制:
A. 实现 Looper 的单例特性
我们知道,一个线程只能有一个 Looper。ThreadLocal 是如何保证这一点的?
-
在
Looper类中,有一个静态的ThreadLocal变量:Looper中定义了sThreadLocal静态变量Javastatic final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); -
Looper.prepare()的逻辑:- 调用
sThreadLocal.get()检查当前线程是否已有Looper。 - 如果发现当前线程已经有了
Looper(即get()不为空),则抛出异常,强制保证单例。 - 如果没有,则创建一个新的
Looper实例,并通过sThreadLocal.set(newLooper)将其存入当前线程的ThreadLocalMap中。
- 调用
通过这种方式,Looper 机制完美地利用了 ThreadLocal 的隔离性,实现了线程内的单例。
Looper 代码片段
Java
public static void prepare() {
prepare(true);
}
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));
}
public void set(T value) {
// 获取当前线程实例
Thread t = Thread.currentThread();
// 从当前线程中获取 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); // ThreadLocal 当作key 把value 保存在 map 中
else
createMap(t, value);
}
ThreadLocal代码片段
Java
public T get() {
// 获取当前线程实例
Thread t = Thread.currentThread();
// 从当前线程中获取 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// this 就是 ThreadLocal,它是一个 Key
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
// 从 Thread 实例中获取 threadLocals 对象
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
B. 性能优化:避免参数传递
ThreadLocal 可以用来在同一线程内的不同方法或模块之间共享数据,而无需层层传递参数。
- 例如,在一个复杂的 IO 操作流程中,你可以把数据库连接对象存入
ThreadLocal,流程中任何地方都可以直接get()拿到,避免了方法签名膨胀。
0x03. 内存泄露风险
使用 ThreadLocal 最危险的陷阱是内存泄露,尤其是在 Android 这种长生命周期的环境(如主线程)中。
A. 风险分析
-
ThreadLocalMap的键是弱引用 (WeakReference<ThreadLocal>)。 -
ThreadLocalMap的值是强引用 (Value)。 -
如果线程一直存活(例如主线程),但外部没有引用指向
ThreadLocal实例,那么:- 外部的
ThreadLocal对象会被 GC 回收(因为是弱引用)。 - 但存储在
ThreadLocalMap里的 Value 仍然被 Map 强引用,无法被回收。 - 键(Key)变成了
null,值(Value)还在,导致内存泄露。
- 外部的
ThreadLocalMap源码片段中可以看到它的 Entry 是一个弱引用
Java
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
....
}
B. 解决方案
为了避免内存泄露,使用完 ThreadLocal 后,务必调用 ThreadLocal.remove() 方法手动清除存储在 Map 中的 Value。
Kotlin
// 错误示例:可能导致内存泄露
val localData = ThreadLocal<HeavyObject>()
localData.set(HeavyObject())
// ... 使用 localData ...
// 忘记调用 remove()
// 正确做法:
val localData = ThreadLocal<HeavyObject>()
try {
localData.set(HeavyObject())
// ... 使用 localData ...
} finally {
// 保证在任何情况下都执行清除操作
localData.remove()
}
0x04. 总结
- 作用: 为每个线程提供独立的变量副本,实现线程安全。
- 原理: 数据存储在每个线程内部的
ThreadLocalMap中。 - 应用: Android 中用于实现
Looper的线程内单例。 - 风险: 必须调用
remove(),否则可能导致内存泄露。