ThreadLocal是一个线程内部的数据存储类,可以在指定线程中存储数据,且只有在该指定线程中才可以获取存储数据。
某个数据是以线程为作用域且不同线程具有不同的 Lopper
如果不采取 ThreadLocal,那么系统就必须提供一个全局的哈希表来 Handler 查找指定线程的 Lopper,这样一来就必须提供一个类似于 LooperManager 的类。
复杂逻辑下的对象传递(如监听器的传递)
有时一个线程中的任务过于复杂,可能表现为函数调用栈比较深以及代码入口的多样性,这时又要监听器能够贯穿整个线程的执行过程。
如果采用 ThreadLocal 可以让监听器作为线程内的全局对象而存在,在线程内部只要通过 get 方法就可以获取监听器。
如果不采取 ThreadLocal,就只能采用另外两种办法:
// 创建 Boolean 类型的 ThreadLocal 对象
ThreadLocal<Boolean> mBooleanThread = new ThreadLocal<Boolean>();
mBooleanThread.set(true); // 主线程中设置为 true
mBooleanThread.get(); // 主线程中获取为 true
new Thread("Thread #1") {
@Override
public void run() {
mBooleanThread.set(false); // 子线程1中设置为 false
mBooleanThread.get(); // 子线程1中获取为 false
}
}.start();
new Thread("Thread #2") {
@Override
public void run() {
// 子线程2中不去设置
mBooleanThread.get(); // 子线程2中获取为 null
}
}.start();
从 ThreadLocal 的 set()
和 get()
方法可以看出,他们所操作的对象都是当前线程的 localValues 对象和 table 数组,因此在不同线程中访问同一个 ThreadLocal 的 set()
和 get()
方法,它们对 ThreadLocal 所做的读写操作仅限于各自内部,这就是为什么 ThreadLocal 可以在多个线程找那个互不干扰的存储和修改数据。
每个 ThreadLocal 都创建一个 ThreadLocalMap,用 Thread 作为 Map 的key,要存储的局部变量作为 Map 的 value。
每个 Thread 维护一个 ThreadLocalMap,用 ThreadLocal 实例本身 Map 的 key,要存储的局部变量作为 Map 的 value。
【优点】
方法声明 | 描述 |
---|---|
protected T initialValue() | 返回当前线程局部变量的初始值 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
先获取当前线程的 ThreadLocalMap 变量,如果存在则设置值,不存在则创建并设置值。
// 设置当前线程绑定的局部变量
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象所维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// map不为空,则设置或更新值
map.set(this, value);
} else {
// map 为空,则为线程t创建一个ThreadLocalMap对象,并把value存放其中
createMap(t, value);
}
}
// 获取线程所维护的ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 为线程创建一个ThreadLocalMap对象,并赋予初始值
void createMap(Thread t, T firstValue) {
// 这里的this是调用此方法的ThreadLocal对象
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值 null
。
// 获取当前线程绑定的局部变量
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象所维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 不为空,则获取值。WeakReference
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 强转并返回value
T result = (T)e.value;
return result;
}
}
// map不存在 或 map 里面没有与当前ThreadLocal关联的值
return setInitialValue();
}
// 设置初始值
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// map不为空,则设置或更新值
map.set(this, value);
} else {
// map为空,则为线程t创建一个ThreadLocalMap对象,并把value存放其中
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
// 返回初始值
// 延迟调用,在调用set方法之前先调用get方法时才会执行。
protected T initialValue() {
// 如果想返回除null之外的其他初始值,可以重写此方法
return null;
}
先获取当前线程的 ThreadLocalMap 变量,如果存在则移除。
// 移除当前线程绑定的局部变量
public void remove() {
// 获取当前线程对象所维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
// map不为空,则移除对应的实体
m.remove(this);
}
}
ThreadLocalMap 是 ThreadLocal 的静态内部类,并没有实现 Map 接口。
// 初始容量,必须是2的幂
private static final int INITIAL_CAPACITY = 16;
// 用于存放数据的table,长度必须是2的幂
private Entry[] table;
// 数组里面元素的个数,用于判断table的当前使用量是否超过阈值
private int size = 0;
// 进行扩容的阈值,当使用量大于它是就要进行扩容
private int threshold; // Default to 0
// 继承自WeakReference,将ThreadLocal对象的生命周期与线程的生命周期解绑
// 如果key为null(entry.get() == null)则表示key不再被引用了,此时entry也可以从table中清除掉
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
// 只能使用ThreadLocal作为key,来存储K-V结构的数据
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap 中的 key 使用了强引用,会导致 threadLocal 和 value 出现内存泄漏。
假设在业务代码中使完 ThreadLocal,threadLocalRef被回收了。
由于 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收。
在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,始终有引用链 threadRef -> currentThread -> threadLocalMap -> entry,Entry就不会被回收,导致Entry内存泄漏(threadLocal 和 value 同时出现内存泄漏)。
ThreadLocalMap 中的 key 使用了弱引用,会导致 value 出现内存泄漏。
【解决办法】
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table数组,长度为16
table = new Entry[INITIAL_CAPACITY];
// 计算索引【重点分析】
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 在相应索引位置存放对应的值
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设置阈值
setThreshold(INITIAL_CAPACITY);
}
【firstKey.threadLocalHashCode】
// 这个值跟斐波那契数列(黄金分割数)有关。
// 目的是让哈希码能均匀的分布在2的n次方的数组里,即Entry[],可以尽量避免hash冲突。
private static final int HASH_INCREMENT = 0x61c88647;
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static int nextHashCode() {
// 每次获取值时,把当前值加上HASH_INCREMENT并返回
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
【& (INITIAL_CAPACITY - 1)】
计算 hash 时,采用 hashCode & (size - 1)
的算法,是取模运算 hashCode % size
的高效实现。
正因为这种算法,要求 size
必须是2的整次幂,保证在索引不越界的前提下,使得 hash 发生冲突的次数减小。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算索引【重点分析】
int i = key.threadLocalHashCode & (len-1);
// 使用线性探测法查找元素【重点分析】
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 对应的key存在,则直接覆盖旧的值
if (k == key) {
e.value = value;
return;
}
// key为null,值不为null,说明之前的ThreadLocal对象已经被回收了
if (k == null) {
// 使用新的元素替换旧的元素
replaceStaleEntry(key, value, i);
return;
}
}
// 探测完毕都没找到,则在空元素位置创建一个新的Entry
tab[i] = new Entry(key, value);
// 叠加一个数量
int sz = ++size;
// cleanSomeSlots用于清除那些e.get())==null的元素
// 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null.
// 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行
// rehash (执行一次全表的扫描清理工作)
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// 获取环形数组的下一个索引
private static int nextIndex(int i, int len) {
// 如果当前索引+1之后越界了,则返回索引0
return ((i + 1 < len) ? i + 1 : 0);
}
【执行流程】
key
计算出索引i
,然后查找i
位置上的Entry。key
等于传入的key
,则直接给这个Entry赋新的value
值。key
等于null
,则调用replaceStaleEntry
来更换这个key
为null
的Entry。null
的地方,此时如果循环还没有return
,就在这个null
的位置新建一个Entry,并且插入,同时size
增加1。cleanSomeSlots
,清理key
为null
的Entry。size
是否达到了rehash
的条件(size≥thresgold),达到的话就会调用rehash
函数执行一次全表的扫描清理。【线性探测法】
用来解决哈希冲突。
table[14]
上已经有值,且其key与当前key不一致,那么就发生了hash冲突,这时将14加1得到15,取table[15]
进行判断,如果还冲突会回到0,取table[0],以此类推直到可以插入为止。Entry[] table
看成一个环形数组。