ThreadLocal用于线程间的数据隔离。应用场合,最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。
ThreadLocal是一个本地线程副本变量工具类。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
JDK8 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的key是ThreadLocal实例本身,value才是真正要存储的值Object。
ThreadLocalMap是ThreadLocal内部类,由ThreadLocal创建,Thread有ThreadLocal.ThreadLocalMap类型的属性。
一个线程只维护一个ThreadLocalMap哈希表,其key是ThreadLocal实例,所以如果ThreadLocal是全局变量,则key只有一个,所以对应的Map的size为1(这种用法应该较常见)。如果有局部ThreadLocal变量,则ThreadLocalMap的元素可能有多个。
步骤:
关键在于上面的第二步,ThreadLocalMap的set(ThreadLocal> key, Object value)方法!
// ThreadLocal.java
public void set(T value) {
// 返回当前执行的线程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// key是ThreadLocal对象
map.set(this, value);
else
createMap(t, value);
}
// getMap方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 返回的map是Thread类中定义的
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
// Thread.java,每个Thread内部维护着一个ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
// 如果返回的map不为空,则以当前ThreadLocal对象为key,set进map中
// 如果返回的map为空,则创建一个,如下所示
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 初始化ThreadLocalMap。ThreadLocalMap的构造方法是延迟加载的,也就是说,只有当线程需要存储对应的ThreadLocal的值时,才初始化创建一次。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];//初始容量16
// ThreadLocal 的 hash 值 threadLocalHashCode与容量 进行&运算(2个数字进行&时,运算后的值小于2数之间小的那个)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;//size为1
setThreshold(INITIAL_CAPACITY);//阈值 = 16 * 2 / 3;
}
ThreadLocalMap的set方法:
private void set(ThreadLocal<?> key, Object value) {
// ThreadLocalMap的table属性
Entry[] tab = table;
int len = tab.length;
// 计算对应threalocal的存储位置
int i = key.threadLocalHashCode & (len-1);
// 循环遍历table对应该位置的实体,查找对应的threadLocal
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {//每次+1
// 获取当前位置的ThreadLocal
ThreadLocal<?> k = e.get();
// 如果key threadLocal一致,则证明找到对应的threadLocal,直接设置
if (k == key) {
e.value = value;
return;
}
// 如果当前位置的key threadLocal为null,替换过期的
// key 为 null,则证明引用已经不存在;这是因为Entry继承的是WeakReference
if (k == null) {
// 删除过期的entry
replaceStaleEntry(key, value, i);
return;
}
}
// 当前位置的k != key && k != null,创建新的实体,并存放至当前位置i
tab[i] = new Entry(key, value);
// 实际存储键值对元素个数 + 1,entries in the table.
int sz = ++size;
// 清除无用数据&扩容逻辑
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
主要操作为第二步,用ThreadLocalMap的getEntry(ThreadLocal> key)方法获取某一存在key的实体 entry
public T get() {
Thread t = Thread.currentThread();
// getMap和set方法中一致
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 设置初始值为null
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
ThreadLocalMap的getEntry方法:
private Entry getEntry(ThreadLocal<?> key) {
// 要获取的entry的存储位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 存在对应实体Entry并且对应key相等,即同一ThreadLocal
if (e != null && e.get() == key)
return e;
else
// 不存在对应实体Entry 或者 key不相等,则继续查找...
return getEntryAfterMiss(key, i, e);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
ThreadLocalMap的底层实现是一个定制的自定义HashMap哈希表。
// 没有实现Map接口,用独立的方式实现了Map的功能
static class ThreadLocalMap {
// 用Entry来保存K-V结构数据
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
// Entry中key只能是ThreadLocal对象,Entry的构造方法已经指定
Entry(ThreadLocal<?> k, Object v) {
// 调用public WeakReference(T referent),弱引用
super(k);
value = v;
}
}
Entry继承自WeakReference弱引用,生命周期只能存活到下次GC前。但只有Key是弱引用类型的,Value并非弱引用。
ThreadLocalMap的成员变量:
// 初始容量
private static final int INITIAL_CAPACITY = 16;
// 底层哈希表 table, 必要时需要进行扩容,底层哈希表 table.length 长度必须是2的n次方。
private Entry[] table;
// 实际存储键值对元素个数 entries
private int size = 0;
// 下一次扩容时的阈值
private int threshold; // Default to 0
Entry[] table;哈希表存储的核心元素是Entry,Entry包含:
ThreadLocal> k;:当前存储的ThreadLocal实例对象
Object value;:当前 ThreadLocal 对应储存的值value
冲突的解决:
ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry(key=null) -> value, 永远无法回收,造成内存泄漏。
官方防护:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
具体实践中,每次使用完ThreadLocal,都调用它的remove()方法,清除数据。养成习惯就像加锁完要解锁一样,用完就清理。
参考:
ThreadLocal-面试必问深度解析
JAVA并发-自问自答学ThreadLocal