深入分析 ThreadLocal

这篇文章主要分析 Android 中的 ThreadLocal 原理以及相关问题,也分析与其在 Java 中内部实现的区别,让大家理解 ThreadLocal 的使用场景与正确使用方法。

ThreadLocal 的定义

Android 源码中描述:

Implements a thread-local storage, that is, a variable for which each thread
has its own value. All threads share the same {@code ThreadLocal} object,
but each sees a different value when accessing it, and changes made by one
thread do not affect the other threads. The implementation supports
{@code null} values.

实现了线程局部变量的存储。所有线程共享同一个 ThreadLocal 对象,但是每个线程只能访问和修改自己存储的变量,不会影响其他线程。此实现支持存储 null 变量。

从上面的定义看出,关键的地方即:ThreadLocal 对象是多线程共享的,但每个线程持有自己的线程局部变量。ThreadLocal 不是用来解决共享对象问题的,而是提供线程局部变量,让线程之间不会互相干扰。

下面看在 Android 中 Looper 的应用,每个线程只有一个 Looper 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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));
}
/**
* Return the Looper object associated with the current thread. Returns
* null if the calling thread is not associated with a Looper.
*/
public static Looper myLooper() {
return sThreadLocal.get();
}

在了解 ThreadLocal 的作用后,也会产生一些疑问:

线程局部变量是怎么存储的?

是怎么做到线程间相互独立的?

接下来在分析 Android 的 ThreadLocal 源码的过程中,理解其实现原理,并解决上面的疑问。

ThreadLocal 的实现原理

ThreadLocal 有三个主要的 public 方法:set, get, remove。

ThreadLocal 是通过 set 方法存储局部变量的,所以先从 set 方法看起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
/**
* Gets Values instance for this thread and variable type.
*/
Values values(Thread current) {
return current.localValues;
}
/**
* Creates Values instance for this thread and variable type.
*/
Values initializeValues(Thread current) {
return current.localValues = new Values();
}

set 方法中根据当前线程获得Values,线程局部变量也是存储在Values中,而不是 ThreadLocal 对象中。如果一开始 values 为null,就通过 initializeValues 方法初始化。上面代码根据线程获得的values变量就是 Thread 对象的 localValues 变量,可看下 Thread 源码中相关部分:

1
2
3
4
5
6
public class Thread implements Runnable {
/**
* Normal thread local values.
*/
ThreadLocal.Values localValues;
}

接下来来看 Values 的定义,了解其内部结构,进一步清楚线程局部变量的存储细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Per-thread map of ThreadLocal instances to values.
*/
static class Values {
...
/**
* Map entries. Contains alternating keys (ThreadLocal) and values.
* The length is always a power of 2.
*/
private Object[] table;
...
}

Values 是用数组来存储 ThreadLocal 和对应的 value 的,保存一个线程中不同 ThreadLocal 以及局部变量。Values 类内部具体的细节,推荐阅读 由浅入深全面剖析 ThreadLocal。其实 table 数组中没有保存 ThreadLocal 的强引用,而是 ThreadLocal 的 reference 变量,实际上就是保存 ThreadLocal 的弱引用。

1
2
3
/** Weak reference to this thread local instance. */
private final Reference<ThreadLocal<T>> reference
= new WeakReference<ThreadLocal<T>>(this);

到这里就可以回答之前提到的两个问题,线程局部变量是存储在 Thread 的 localValues 属性中,以 ThreadLocal 的弱引用作为 key,线程局部变量作为 value。虽然每个线程共享同一个 ThreadLocal 对象,但是线程局部变量都是存储在线程自己的成员变量中,以此保持相互独立。

ThreadLocal 的 get 方法的默认值

get 方法就是取出 Vaules 中对应的线程局部变量,需要注意的是在没有 set 的情况下,调用 get 方法返回的默认值是 null,这其实是有 initialValue 方法确定的,可以重写。

1
2
3
4
5
6
7
8
9
/**
* Provides the initial value of this variable for the current thread.
* The default implementation returns {@code null}.
*
* @return the initial value of the variable.
*/
protected T initialValue() {
return null;
}

Java 中的 ThreadLocal 有什么区别

Java 的 ThreadLocal 源码与 Android 中的 ThreadLocal 不太一样,不过大致的实现原理是一样的,Android 中 ThreadLocal 稍微优化了一下,更节约内存。两者最大的区别就是存储局部变量的 Values 类在 Java 中是 ThreadLocalMap 类,内部的存储方式有些不同,Java中用 ThreadLocal.ThreadLocalMap.Entry 来封装 key 和 value。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static class ThreadLocalMap {
private static final int INITIAL_CAPACITY = 16;
private ThreadLocal.ThreadLocalMap.Entry[] table;
private int size;
private int threshold;
...
static class Entry extends WeakReference<ThreadLocal> {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
this.value = v;
}
}
}

两者都是以 ThreadLocal 弱引用作为 key 值。

ThreadLocal 的内存泄漏问题

网上有讨论说 ThreadLocal 有可能出现内存泄漏问题,这的确是有可能的。

现在看下线程局部变量的引用链:Thread.localValues -> WeakReference 和 value。如果没有其他对象引用 ThreadLocal 对象的话,ThreadLocal 可能会被回收,但是 value 不会被回收,value是强引用。所以没有显式地调用 remove 的话,的确有可能发生内存泄漏问题。

不过 ThreadLocal 的设计者也考虑到这个问题,在 get 或 set 方法中会检测 key 是否被回收,如果是的话就将 value 设置为 null,具体是调用 Values 的 cleanUp 方法实现的。这种设计可以避免多数内存泄漏问题,但是极端情况下,ThreadLocal 对象被回收后,也没有调用 get 或 set 方法的话,还是会发生内存泄漏。

现在回过来看,这种情况的发生都是基于没有调用 remove 方法,而 ThreadLocal 的正确使用方式是在不需要的时候 remove,这样就不会出现内存泄漏的问题了。

线程局部变量真的只能被一个线程访问?

ThreadLocal 的子类 InheritableThreadLocal 可以突破这个限制,父线程的线程局部变量在创建子线程时会传递给子线程。

看下面的示例,子线程可以获得父线程的局部变量值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void testInheritableThreadLocal() {
final ThreadLocal<String> threadLocal = new InheritableThreadLocal();
threadLocal.set("testStr");
Thread t = new Thread() {
@Override
public void run() {
super.run();
Log.i(LOGTAG, "testInheritableThreadLocal = " + threadLocal.get());
}
};
t.start();
}
// 输出结果为 testInheritableThreadLocal = testStr

具体的实现逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Thread implements Runnable {
...
/**
* Normal thread local values.
*/
ThreadLocal.Values localValues;
/**
* Inheritable thread local values.
*/
ThreadLocal.Values inheritableValues;
...
// 线程创建会调用的方法
private void create(ThreadGroup group, Runnable runnable, String threadName, long stackSize) {
...
// Transfer over InheritableThreadLocals.
if (currentThread.inheritableValues != null) {
inheritableValues = new ThreadLocal.Values(currentThread.inheritableValues);
}
// add ourselves to our ThreadGroup of choice
this.group.addThread(this);
}
}

使用建议

  • ThreadLocal 变量本身定位为要被多个线程访问,所以通常定义为 static

  • 在线程池的情况下,在 ThreadLocal 业务周期结束后,最好显示地调用 remove 方法

END
Johnny Shieh wechat
我的公众号,不只有技术,还有咖啡和彩蛋!