本篇总结ThreadLocal的相关知识,它在实际开发中使用频率较高,要认真学 ~
1 ThreadLocal
- ThreadLocal是一种类型,用于提供 线程局部变量,在多线程环境下可以 保证各个线程里的变量独立于其他线程的变量。
- 即:ThreadLocal可为每个线程创建一个单独的变量副本,相当于线程的private static类型变量。
- 在多线程环境下,同步机制是为了保证数据的一致性,而ThreadLocal是为了保证数据的独立性。
2 ThreadLocal实现
public void set(T value)
public T get()
public void remove()
protected T initialValue()
-
set(T value)方法
- 首先获取当前线程,然后再获取到当前线程的ThreadLocalMap,若它不为null,则将value保存其中,并用当前的ThreadLocal作为key;否则创建一个ThreadLocalMap并给到当前线程,然后保存value。
- ThreadLocalMap相当于一个HashMap,是真正保存值的地方。
-
get()方法
- 在 get() 方法中也会获取到当前线程的 ThreadLocalMap,如果 ThreadLocalMap 不为 null,则获取 key 为当前 ThreadLocal 的值;
- 否则调用 setInitialValue() 方法返回初始值,并保存到新创建的 ThreadLocalMap 中。
-
remove()方法
- 删除当前key
3 ThreadLocalMap
-
在 set,get,initialValue 和 remove 方法中都会获取到当前线程,然后通过当前线程获取到 ThreadLocalMap,如果ThreadLocalMap 为 null,则会创建一个 ThreadLocalMap,并给到当前线程。
-
可见,每个线程都会持有一个ThreadLocalMap用来维护线程本地的值。
-
每个线程的ThreadLocalMap 是属于线程自己的, ThreadLocalMap 中维护的值也是属于线程自己的。这就保证了ThreadLocal 类型的变量在每个线程中是独立的,在多线程环境下不会相互影响。
-
构造方法
ThreadLocal 中当前线程的 ThreadLocalMap 为 null 时会使用ThreadLocalMap 的构造方法新建一个ThreadLocalMap:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//在构造方法中新建一个数组,并将第一次需保存的键值存储到一个数组中,完成初始化。
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
- 存储结构
ThreadLocalMap 内部维护一个哈希表(数组)来存储数据,并且定义了加载因子。
// 初始容量,必须是 2 的幂
private static final int INITIAL_CAPACITY = 16;
// 存储数据的哈希表
//这个table是一个Entry类型的数组,Entry是ThreadLocalMap的一个内部类。
//Entry 用于保存一个键值对,其中key以“弱引用”的方式保存。
private Entry[] table;
// table 中已存储的条目数
private int size = 0;
// 表示一个阈值,当 table 中存储的对象达到该值时就会扩容
private int threshold;
// 设置 threshold 的值
private void setThreshold(int len){
threshold = len *2/3;
}
-
保存键值对
调用
set(ThreadLocal key,Object value)
方法将数据保存到哈希表中。
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算要存储的索引位置
int i = key.threadLocalHashCode & (len-1);
// 循环判断要存放的索引位置是否已经存在 Entry,若存在,进入循环体
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
// 若索引位置的 Entry 的 key 和要保存的 key 相等,则更新该 Entry 的值
if (k == key) {
e.value = value;
return;
}
// 若索引位置的 Entry 的 key 为 null(key 已经被回收了),表示该位置的 Entry 已经无效,用要保存的键值替换该位置上的 Entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 要存放的索引位置没有 Entry,将当前键值作为一个 Entry 保存在该位置
tab[i] = new Entry(key, value);
// 增加 table 存储的条目数
int sz = ++size;
// 清除一些无效的条目并判断 table 中的条目数是否已经超出阈值
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); // 调整 table 的容量,并重新摆放 table 中的 Entry
}
-
首先使用 key(当前 ThreadLocal)的 threadLocalHashCode 来计算要存储的索引位置 i。threadLocalHashCode 的值由ThreadLocal 类管理,每创建一个 ThreadLocal 对象都会自动生成一个相应的 threadLocalHashCode 值。
-
在保存数据时,如果索引位置有 Entry,且该 Entry 的 key 为 null,就会清除无效 Entry 来腾出空间,因为:Entry的 key 是弱引用,key 一旦被回收(即 key 为 null),就无法再访问到 key 对应的 value。
-
在调整 table 容量时,也会先清除无效对象,然后再根据需要扩容。
-
获取Entry对象
- 使用
getEntry(ThreadLocal key)
方法。 - 因为可能存在哈希冲突,key对应的Entry的存储位置可能不在由key计算出的索引位置上,所以要调用
getEntryAfterMiss(ThreadLocal key,int i,Entry e)
方法获取。
- 使用
-
内存泄露
- 在ThreadLocalMap的set、get、remove方法中都有清除无效Entry的操作,和Entry的key使用弱引用的方式,都是为了降低内存泄漏的概率,但不能完全避免。
- 开发中每次用完ThreadLocal 都要调用remove方法,清除所有key为null的value,防止内存泄漏。