这篇文章主要分析 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 对象:
|
|
在了解 ThreadLocal 的作用后,也会产生一些疑问:
线程局部变量是怎么存储的?
是怎么做到线程间相互独立的?
接下来在分析 Android 的 ThreadLocal 源码的过程中,理解其实现原理,并解决上面的疑问。
ThreadLocal 的实现原理
ThreadLocal 有三个主要的 public 方法:set, get, remove。
ThreadLocal 是通过 set 方法存储局部变量的,所以先从 set 方法看起:
|
|
set 方法中根据当前线程获得Values
,线程局部变量也是存储在Values
中,而不是 ThreadLocal 对象中。如果一开始 values 为null,就通过 initializeValues 方法初始化。上面代码根据线程获得的values
变量就是 Thread 对象的 localValues 变量,可看下 Thread 源码中相关部分:
|
|
接下来来看 Values 的定义,了解其内部结构,进一步清楚线程局部变量的存储细节:
|
|
Values 是用数组来存储 ThreadLocal 和对应的 value 的,保存一个线程中不同 ThreadLocal 以及局部变量。Values 类内部具体的细节,推荐阅读 由浅入深全面剖析 ThreadLocal。其实 table 数组中没有保存 ThreadLocal 的强引用,而是 ThreadLocal 的 reference 变量,实际上就是保存 ThreadLocal 的弱引用。
|
|
到这里就可以回答之前提到的两个问题,线程局部变量是存储在 Thread 的 localValues 属性中,以 ThreadLocal 的弱引用作为 key,线程局部变量作为 value。虽然每个线程共享同一个 ThreadLocal 对象,但是线程局部变量都是存储在线程自己的成员变量中,以此保持相互独立。
ThreadLocal 的 get 方法的默认值
get 方法就是取出 Vaules 中对应的线程局部变量,需要注意的是在没有 set 的情况下,调用 get 方法返回的默认值是 null,这其实是有 initialValue 方法确定的,可以重写。
|
|
Java 中的 ThreadLocal 有什么区别
Java 的 ThreadLocal 源码与 Android 中的 ThreadLocal 不太一样,不过大致的实现原理是一样的,Android 中 ThreadLocal 稍微优化了一下,更节约内存。两者最大的区别就是存储局部变量的 Values 类在 Java 中是 ThreadLocalMap 类,内部的存储方式有些不同,Java中用 ThreadLocal.ThreadLocalMap.Entry 来封装 key 和 value。
|
|
两者都是以 ThreadLocal 弱引用作为 key 值。
ThreadLocal 的内存泄漏问题
网上有讨论说 ThreadLocal 有可能出现内存泄漏问题,这的确是有可能的。
现在看下线程局部变量的引用链:Thread.localValues -> WeakReference
不过 ThreadLocal 的设计者也考虑到这个问题,在 get 或 set 方法中会检测 key 是否被回收,如果是的话就将 value 设置为 null,具体是调用 Values 的 cleanUp 方法实现的。这种设计可以避免多数内存泄漏问题,但是极端情况下,ThreadLocal 对象被回收后,也没有调用 get 或 set 方法的话,还是会发生内存泄漏。
现在回过来看,这种情况的发生都是基于没有调用 remove 方法,而 ThreadLocal 的正确使用方式是在不需要的时候 remove,这样就不会出现内存泄漏的问题了。
线程局部变量真的只能被一个线程访问?
ThreadLocal 的子类 InheritableThreadLocal 可以突破这个限制,父线程的线程局部变量在创建子线程时会传递给子线程。
看下面的示例,子线程可以获得父线程的局部变量值:
|
|
具体的实现逻辑:
|
|
使用建议
ThreadLocal 变量本身定位为要被多个线程访问,所以通常定义为 static
在线程池的情况下,在 ThreadLocal 业务周期结束后,最好显示地调用 remove 方法