ThreadLocal 简介
ThreadLocal 是一个线程内部的数据存储类,是 JDK java.lang 包中的一个用来实现相同线程数据共享不同的线程数据隔离的一个工具。通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其它线程来说则无法获取到数据。在日常开发中用到 ThreadLocal 的地方较少,但是在某些特殊的场景下,通过 ThreadLocal 可以轻松地实现一些看起来很复杂的功能,这一点在 Android 的源码中也有所体现,比如 Looper、ActivityThread 以及 AMS 中都用到了 ThreadLocal。具体到 ThreadLocal 的使用场景,一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用 ThreadLocal。比如对于 Handler 来说,它需要获取当前线程的 Looper,很显然 Looper 的作用域就是线程并且不同线程具有不同的 Looper,这个时候通过 ThreadLocal 就可以轻松实现 Looper 在线程中的存取。如果不采用 ThreadLocal,那么系统就必须提供一个全局的哈希表供 Handler 查找指定线程的 Looper,这样一来就必须提供一个类似于 LooperManager 的类了,但是系统并没有这么做而是选择了 ThreadLocal,这就是 ThreadLocal 的好处。
ThreadLocal 另一个使用场景是复杂逻辑下的对象传递,比如监听器的传递,有些时候一个线程中的任务过于复杂,这可能表现为函数调用栈比较深以及代码入口的多样性,在这种情况下,我们又需要监听器能够贯穿整个线程的执行过程,这个时候就可以采用 ThreadLocal,采用 ThreadLocal 可以让监听器作为线程内的全局对象而存在,在线程内部只要通过 get 方法就可以获取到监听器。如果不采用 ThreadLocal,那么我们能想到的可能是如下两种方法:第一种方法是将监听器通过参数的形式在函数调用栈中进行传递,第二种方法就是将监听器作为静态变量供线程访问。上述这两种方法都是有局限性的。第一种方法的问题是当函数调用栈很深的时候,通过函数参数来传递监听器对象这几乎是不可接受的,这会让程序的设计看起来很糟糕。第二种方法是可以接受的,但是这种状态是不具有可扩充性的,比如同时有两个线程在执行,那么就需要提供两个静态的监听器对象,如果有10个线程在并发执行呢?提供10个静态的监听器对象?这显然是不可思议的,而采用 ThreadLocal,每个监听器对象都在自己的线程内部存储,根本就不会有方法2的这种问题。
此外,要保证线程安全,并不是一定就需要同步,两者没有因果关系,同步只是保证共享数据征用时正确性的手段,如果一个方法本来就不涉及共享数据,那它就不需要任何同步措施去保证正确性。ThreadLocal 的概念就是从这里引申出来的。
ThreadLocal 使用实例
实例一
请看下面 ThreadLocal 的使用:
普通变量:
1 | import java.util.concurrent.CountDownLatch; |
程序的运行的随机结果如下:
1 | thread - 1 |
从结果我们可以看出多个线程在访问同一个变量的时候出现的异常,线程间的数据没有隔离。下面我们来看采用 ThreadLocal 变量的方式来解决这个问题的例子:
1 | import java.util.concurrent.CountDownLatch; |
程序运行结果如下:
1 | thread - 0 |
从结果来看,这次很好的解决了多线程之间数据隔离的问题,十分方便。这里可能有的朋友会觉得在例子1中我们完全可以通过加锁来实现这个功能。加锁确实可以解决这个问题,但是在这里我们强调的是线程数据隔离的问题,并不是多线程共享数据的问题。假如我们这里除了 getString() 之外还有很多其它方法也要用到这个 String,这个时候各个方法之间就没有显式的数据传递过程了,都可以直接从 ThreadLocal 变量中获取,这才是 ThreadLocal 的核心,相同线程数据共享不同的线程数据隔离。由于ThreadLocal 是支持泛型的,这里采用的是存放一个 String 来演示,其实可以存放任何类型,效果都是一样的。
实例二
下面再通过实例二来演示 ThreadLocal 的真正含义。首先定义一个 ThreadLocal 对象,这里选择 Boolean 类型的,如下所示:
1 | private ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<Boolean>(); |
然后分别在主线程、子线程1和子线程2中设置和访问它的值,代码如下所示(详见ThreadLocalTestActivity):
1 | mBooleanThreadLocal.set(true); |
在上面的代码中,在主线程中设置 mBooleanThreadLocal 的值为 true,在子线程1中设置 mBooleanThreadLocal 的值为 false,在子线程2中不设置 mBooleanThreadLocal 的值。然后分别在3个线程中通过 get 方法获取 mBooleanThreadLocal 的值,根据前面对 ThreadLocal 的描述,这个时候,主线程中应该是 true,子线程1中应该是 false,而子线程2中由于没有设置值,所以应该是 null。运行程序,日志如下所示。
1 | D/TestActivity(8676): [Thread#main]mBooleanThreadLocal=true |
从上面日志可以看出,虽然在不同线程中访问的是同一个 ThreadLocal 对象,但是它们通过 ThreadLocal 获取到的值却是不一样的。ThreadLocal 之所以有这么奇妙的效果,是因为不同线程访问同一个 ThreadLocal 的 get 方法,ThreadLocal 内部会从各自的线程中取出一个数组,然后再从数组中根据当前 ThreadLocal 的索引去查找出对应的 value 值。很显然,不同线程中的数组是不同的,这就是为什么通过 ThreadLocal 可以在不同的线程中维护一套数据的副本并且彼此互不干扰。
ThreadLocal 图解
ThreadLocal 工作流程图
每一个 ThreadLocal 对象通过 Thread.currentThread 得到当前 Thread 对象,而 Thread 对象中引用了 ThreadLcoalMap 对象,ThreadLocalMap对象又引用了 Entry 数组,Entry 数组中的每一个 Entry 对象是由 ThreadLocal 对象和变量值 Value 组成。遍历每一个 Entry 对象,比对其中的 ThreadLocal 对象是否和最上层传递进来的 ThreadLocal 对象相等,若相等,则返回其中的变量值 Value。
ThreadLocal 结构图
下面分析 ThreadLocal 的内部实现,ThreadLocal 是一个泛型类,它的定义为 public class ThreadLocal<T>,它的结构如下图所示:
一个线程 Thread 中存在一个 ThreadLocalMap,ThreadLocalMap 中的 key 对应 ThreadLocal,在此处可见 Map 可以存储多个 key 即(ThreadLocal)。另外 Value 就对应着在 ThreadLocal 中存储的 Value。
ThreadLocal 工作原理图解
多线程操作同一对象情况,如图所示:
使用 ThreadLocal 定义的变量,将指向当前线程本地的一个 LocalMap 空间。ThreadLocal 变量作为 key,其内容作为 value,保存在本地。多线程对 ThreadLocal 对象进行操作,实际上是对各自的本地变量进行操作,不存在线程安全问题,如图所示:
Thread 类中有一个成员变量属于 ThreadLocalMap 类(一个定义在 ThreadLocal 类中的内部类),它的 key 是 ThreadLocal 实例对象。当为 ThreadLocal 类的对象 set 值时,首先获得当前线程的 ThreadLocalMap 类属性,然后以 ThreadLocal 类的对象为 key,设定 value。get 值时则类似。ThreadLocal 变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成。也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为 ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其它线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。由 ThreadLocal 的工作原理决定了每个线程独自拥有一个变量,并非是共享的。
ThreadLocal 工作原理可以简单的概况为:每个线程都会唯一绑定一个 ThreadLocalMap 的对象,而 ThreadLocalMap 可以存储以 ThreadLocal 为 key 的键值对。这里解释了为什么每个线程访问同一个 ThreadLocal,得到的确是不同的数值。
ThreadLocal 源码分析
ThreadLocal 的 set 方法
1 | public void set(T value) { |
如上所示,在 set 方法中首先获取当前线程,然后通过 getMap 获取到当前线程的 ThreadLocalMap 类型的变量 threadLocals,如果存在则直接赋值,如果不存在则给该线程创建 ThreadLocalMap 变量并赋值。在注释1处,得到 map 对象之后,用 this 作为 key,this 在这里代表的是当前线程的 ThreadLocal 对象。另外就是第二句根据 getMap 获取一个 ThreadLocalMap,其中 getMap 中传入了参数 t(当前线程对象),这样就能够获取每个线程的 ThreadLocal 了。
ThreadLocal 的 get 方法
1 | public T get() { |
get 方法先获取当前线程的 ThreadLocalMap 变量,如果存在,则使用当前 ThreadLoacl 对象(this)获取 ThreadLocalMap 的 Entry 对象,返回 Entry 保存的 value 值。不存在则创建并返回初始值,setInitialValue() 方法中,最后返回的是 value,而 value 来自 initialValue(),可以看出如果不设置 ThreadLocal 的数值,默认就是 null,来自于此。
从 ThreadLocal 的 set 和 get 方法来看,它们操作的对象都是当前线程对象中的 ThreadLocalMap 对象的 Entry[] 数组,因此在不同的线程中访问同一个 ThreadLocal 的 set 和 get 方法,操作的对应线程中的数据,所以不会影响到其他线程。
ThreadLocalMap 类结构和成员变量
ThreadLocalMap 是 ThreadLocal 的一个内部类,ThreadLocal 的底层实现都是通过 ThreadLocalMap 来实现的。
1 | static class ThreadLocalMap { |
ThreadLocalMap 中使用 Entry[] 数组来存放对象实例与变量的关系,并且实例对象作为 key,变量作为 value 实现对应关系。Entry 继承了 WeakReference
ThreadLocalMap set 和 getEntry 方法
1 | private void set(ThreadLocal<?> key, Object value) { |
上面的代码实现了数据的存储,其中 table 是一个 Entry[] 数组对象,而 Entry 是用来存储 ThreadLocal key, Object value 的,逻辑是根据 key 找出 Entry 对象,如果找出的这个 Entry 的 k 等于 key,直接设置 Entry 的 value,如果 k 为空,则通过 replaceStaleEntry 保存数据,最后构建出 Entry 保存进 table 数组中。
1 | //获取 Entry |
ThreadLocal 内存泄露
每个 Thread 中都存在一个类型是 ThreadLocal.ThreadLocalMap 的 Map,Map 中的 key 为一个 Threadlocal 实例。根据上面 Entry 方法的源码,我们知道 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的。下图是一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:
每个 key 都弱引用指向 Threadlocal,当把 Threadlocal 实例置为 null 以后,没有外部强引用指向该 Threadlocal 实例,那么系统 gc 的时候,这个 ThreadLocal 会被回收。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:
1 | Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value |
只有当前 Thread 结束以后,Current Thread 就不会存在栈中,强引用断开,Current Thread、Map、value 将全部被 gc 回收。所以只要这个线程对象被 gc 回收,就不会出现内存泄露,但在 ThreadLocal 设为 null 和线程结束这段时间不会被回收的,就发生了内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 。
ThreadLocalMap 的 getEntry 函数的流程大概为:
首先从 ThreadLocal 的直接索引位置(通过 ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取 Entry e,如果 e 不为 null 并且 key 相同则返回 e;
如果 e 为 null 或者 key 不一致则向下一个位置查询,如果下一个位置的 key 和当前需要查询的 key 相等,则返回对应的 Entry。否则,如果 key 值为 null,则擦除该位置的 Entry,并继续向下一个位置查询。在这个过程中遇到的 key 为 null 的 Entry 都会被擦除,那么 Entry 内的 value 也就没有强引用链,自然会被回收。仔细研究代码可以发现,set 操作也有类似的思想,将 key 为 null 的这些 Entry 都删除,防止内存泄露。
但是光这样还是不够的,上面的设计思路依赖一个前提条件是要调用 ThreadLocalMap 的 getEntry 函数或者 set 函数。这当然不可能任何情况都成立的,所以很多情况下需要使用者手动调用 ThreadLocal 的 remove 函数,手动删除不再需要的 ThreadLocal,防止内存泄露。所以 JDK 建议将 ThreadLocal 变量定义成 private static 的,这样的话 ThreadLocal 的生命周期就更长,由于一直存在 ThreadLocal 的强引用,所以 ThreadLocal 也就不会被回收,也就能保证任何时候都能根据 ThreadLocal 的弱引用访问到 Entry 的 value 值,然后 remove 它,防止内存泄露。
ThreadLocal 几问
如何实现一个线程多个 ThreadLocal 对象,每一个 ThreadLocal 对象是如何区分的呢?
查看源码,可以看到:
1 | private final int threadLocalHashCode = nextHashCode(); |
对于每一个 ThreadLocal 对象,都有一个 final 修饰的 int 型的 threadLocalHashCode 不可变属性,对于基本数据类型,可以认为它在初始化后就不可以进行修改,所以可以唯一确定一个 ThreadLocal 对象。但是如何保证两个同时实例化的 ThreadLocal 对象有不同的 threadLocalHashCode 属性呢?在 ThreadLocal 类中,还包含了一个 static 修饰的 AtomicInteger(提供原子操作的 Integer 类)成员变量(即类变量)和一个 static final 修饰的常量(作为两个相邻 nextHashCode 的差值)。由于 nextHashCode 是类变量,所以每一次调用 ThreadLocal 类都可以保证 nextHashCode 被更新到新的值,并且下一次调用 ThreadLocal 类这个被更新的值仍然可用,同时 AtomicInteger 保证了 nextHashCode 自增的原子性。
为什么不直接用线程 id 来作为 ThreadLocalMap 的 key?
直接用线程 id 来作为 ThreadLocalMap 的 key,无法区分放入 ThreadLocalMap 中的多个 value。比如我们放入了两个字符串,无法知道要取出来的是哪一个字符串。而使用 ThreadLocal 作为 key 就不一样了,由于每一个 ThreadLocal 对象都可以由 threadLocalHashCode 属性唯一区分或者说每一个 ThreadLocal 对象都可以由这个对象的名字唯一区分,所以可以用不同的 ThreadLocal 作 为 key,区分不同的 value,方便存取。
每个线程的变量副本是存储在哪里的?
从当前线程的 ThreadLocalMap 中取出当前线程对应的变量的副本,变量是保存在线程中的,而不是保存在 ThreadLocal 变量中。当前线程中,有一个变量引用名字是 threadLocals,这个引用是在 ThreadLocal 类中 createmap 函数内初始化的。每个线程都有一个这样的 threadLocals 引用的 ThreadLocalMap,以 ThreadLocal 和 ThreadLocal 对象声明的变量类型作为参数。这样,我们所使用的 ThreadLocal 变量的实际数据,通过 get 函数取值的时候,就是通过取出 Thread中 threadLocals 引用的 map,然后从这个 map 中根据当前 threadLocal 作为参数,取出数据。
参考资料:
《Android 开发艺术探索》任玉刚 第10章 10.2 10.2.1 ThreadLocal的工作原理
聊聊面试中的 ThreadLocal 原理和使用场景
LESS IS MORE 深入理解 Java 之 ThreadLocal 工作原理
崔显龙 图解ThreadLocal工作原理
VC_Hlv-2 ThreadLocal的工作原理
小朔哥的Java路 彻底理解ThreadLocal(看这篇文章就够了)
mingfeng002 Android的消息机制之ThreadLocal的工作原理