前言
本文是介绍Android中的ThreadLocal。
目录
一、ThreadLocal是什么
ThreadLocal 是线程局部(本地)变量,也许把它命名为 ThreadLocalVariable 更容易让人理解一些。官方介绍:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its
getorsetmethod) has its own, independently initialized copy of the variable.ThreadLocalinstances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
ThreadLocal 提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过 get 和 set 方法就可以得到当前线程对应的值。
产生背景:在 Java 的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用 synchronized 来保证同一时刻只有一个线程对共享变量进行操作。当每个线程需要一个独享的对象或者线程内要保存全局变量时,可以将变量放到 ThreadLocal 类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的 ThreadLocal 使用场景为用来解决数据库连接、Session 管理等。
本质:Thread 类有一个 ThreadLocal.ThreadLocalMap 类型的实例变量 threadLocals,ThreadLocal 的静态内部类 ThreadLocalMap 为每个 Thread 都维护了一个数组 table,ThreadLocal 对象确定了一个数组下标,而这个下标就是 value 存储的对应位置。
作用:为解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal 比直接使用 synchronized 同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
二、使用
创建ThreadLocal变量
1 | // 1. 直接创建对象 |
访问ThreadLocal变量
1 | // 1. 设置值:set() |
三、源码分析
set
1 | //set 方法 |
可以看出每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量 threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。
ThreadLocalMap
1 | static class ThreadLocalMap { |
实例化 ThreadLocalMap 时创建了一个长度为16的 Entry 数组。通过 hashCode 与 length 位运算确定出一个索引值i,这个i就是被存储在 table 数组中的位置。每个线程 Thread 持有一个 ThreadLocalMap 类型的实例threadLocals 可理解成每个线程 Thread 都持有一个 Entry 型的数组 table,而一切的读取过程都是通过操作这个数组 table 完成的。
ThreadLocalMap的set
1 | private void set(ThreadLocal<?> key, Object value) { |
在 ThreadLocalMap 中的 set 方法与构造方法能看到以下代码片段。
int i = key.threadLocalHashCode & (len-1)int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
简而言之就是将threadLocalHashCode进行一个位运算(取模)得到索引i,threadLocalHashCode代码如下:
1 | //ThreadLocal中threadLocalHashCode相关代码. |
因为static的原因,在每次new ThreadLocal时因为 threadLocalHashCode 的初始化,会使threadLocalHashCode 值自增一次,增量为0x61c88647。
0x61c88647是斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突。
综上可以得出:
1.对于某一 ThreadLocal 来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的 table 数组的同一位置即都为 table[i],只不过这个不同线程之间的 table 是独立的。
2.对于同一线程的不同 ThreadLocal 来讲,这些 ThreadLocal 实例共享一个 table 数组,然后每个 ThreadLocal 实例在 table 中的索引 i 是不同的。
即 ThreadLocal 中 set 和 get 操作的都是对应线程的 table数组,因此在不同的线程中访问同一个 ThreadLocal 对象的 set 和 get 进行存取数据是不会相互干扰的。
get
通过计算出索引直接从数组对应位置读取即可。
1 | //ThreadLocal中get方法 |
setInitialValue
1 | /** |
initialValue
重写该方法可初始化值,默认情况下,initialValue方法返回的是null。
1 | protected T initialValue() { |
remove
1 | //ThreadLocal中remove方法 |
ThreadLocal如何做到线程安全
- 每个线程拥有自己独立的
ThreadLocals变量(指向ThreadLocalMap对象 ) - 每当线程 访问
ThreadLocals变量时,访问的都是各自线程自己的ThreadLocalMap变量(键 - 值) ThreadLocalMap变量的键key= 唯一 = 当前ThreadLocal实例
与同步机制的区别
ThreadLocal 和 Synchronized 都是为了解决多线程中相同变量的访问冲突问题,不同的点是
- Synchronized 是通过线程等待,牺牲时间来解决访问冲突,以时间换空间
- ThreadLocal 是通过每个线程单独一份存储空间,牺牲空间来解决冲突,以空间换时间,并且相比于Synchronized,ThreadLocal 具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
因为 ThreadLocal 的线程隔离特性,在 android 中Looper、ActivityThread 以及 AMS 中都用到了 ThreadLocal。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用 ThreadLocal。
防止ThreadLocal中的内存泄漏
ThreadLocalMap的键Key = 当前ThreadLocal实例、值value = 该线程设置的存储在ThreadLocal变量的值
该key是 ThreadLocal对象的弱引用;当要抛弃掉ThreadLocal对象时,垃圾收集器会忽略该 key的引用而清理掉ThreadLocal对象
ThreadLocalMap底层会维护一个Entry数组,而Entry本身却是WeakReference的子类,并且在构造器中将ThreadLocal传给了父类WeakReference。也就是说,Entry对于ThreadLocal持有的引用是弱引用,它并不会影响GC对于ThreadLocal对象的回收。
但是对于value 依旧是强引用,如果一个ThreadLocal对象被回收了,我们往里面放的value对于【当前线程->当前线程的threadLocals(ThreadLocal.ThreadLocalMap对象)->Entry数组->某个entry.value】这样一条强引用链是可达的,因此value不会被回收。如果不及时清理释放,是会导致内存泄漏的。所以,我们在不使用时,最好调用 ThreadLocal 的 remove() 方法。
总结
在每个线程 Thread 内部有一个 ThreadLocal.ThreadLocalMap 类型的成员变量 threadLocals,这个threadLocals 就是用来存储实际的变量副本的,键值为当前 ThreadLocal 变量,value 为变量副本值(即T类型对象值)。
初始时,在 Thread 里面,threadLocals 为空,当通过 ThreadLocal 变量调用 get() 方法或者 set() 方法,就会对Thread类中的 threadLocals 进行初始化,并且以当前 ThreadLocal 变量为键值,以 ThreadLocal 要保存的副本变量为 value,存到 threadLocals。 然后在当前线程里面,如果要使用副本变量,就可以通过 get 方法在threadLocals 里面查找。
- 每个Thread维护着一个ThreadLocalMap的引用;
- ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储;
- ThreadLocal创建的副本是存储在线程自己的threadLocals中的,也就是线程自己的ThreadLocalMap;
- ThreadLocalMap的键值为ThreadLocal对象,因为每个线程可以有多个threadLocal变量,因此保存在ThreadLocalMap中,键值为ThreadLocal对象;
- ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
引用文章: