一、概述
ThreadLocal,即线程局部变量。首要用于线程间数据隔离。这些变量在多线程环境下拜访(通过get或set办法拜访)时能确保各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例一般来说都是private static类型。ThreadLocal不是为了解决多线程拜访共享变量,而是为每个线程创立一个单独的变量副本,供给了保持方针的办法和防止参数传递的复杂性。 ThreadLocal的首要应用场景为按线程多实例(每个线程对应一个实例)的方针的拜访,并且这个方针许多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创立一个ThreadLocal,里边存用户基本信息等,在许多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联络并且当时线程也能够及时获取想要的数据。
二、原理
ThreadLocal如何完成线程独立拜访ThreadLocal相关的变量呢?
这儿首要有两种办法:
- 在ThreadLocal中维护一个map,map的key是线程,value是相关的变量。但这种办法不太高雅(JDK1.5之前选用的这种办法),比方说可能会导致线程很大,并且当线程毁掉时,还需求在map中将其删除,在多线程景象下,会添加维护难度和时刻成本。
- 每个Thread维护一个ThreadLoaclMap映射表,这个映射表的key是ThreadLocal实例自身,value是真实需求存储的Object。这样做有许多好处,比方不必加锁来确保读写安全,并且当线程毁掉时,与其相关的ThreadLocalMap也天然消亡。
ThreadLocalMap的阐明
ThreadLocal没有直接运用HashMap而是自己从头开发了一个map,最首要的作用是让他的key为虚引证类型,这样当ThreadLocal方针毁掉时,多个持有其引证的线程不会影响它的回收。 ThreadLocalMap是一个很像HashMap的一个数据结构,但他并没有完成Map接口,并且它的Entry是承继WeakReference的,也没有next指针,所以不存在链表了。对于hash抵触,而是选用的开放地址法来进行解决 ThreadLocaMap的扩容机制也不同于HashMap,ThreadLocalMap的扩容阈值是长度的2/3,当表中的元素数量到达阈值时,不会立即进行扩容,而是会触发一次rehash操作铲除过期数据,假如铲除过期数据之后元素数量大于等于总容量的3/4才会进行真实意义上的扩容
// ThreadLocalMap没有承继Map接口
static class ThreadLocalMap{
// Entry被声明为弱引证
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
// 调用rehas办法
private void rehash() {
// 铲除过期数据
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
// 铲除过期数据后数据依然大于等于总容量的3/4,则扩容
if (size >= threshold - threshold / 4)
resize();
}
get/set/初始化
当调用get或set办法时,首要会去检查线程的ThreadLocalMap是否被初始化,假如没有初始化,则会进行初始化操作,不然依据计算出来的key找到对应下标,假如对应下标是咱们要找的元素,则回来,不然会向后查找,直到碰到slot为null或者找到停止。在这过程中同时会整理过期的K-V对,set同理。具体能够参看源码。
// get办法
public T get() {
// 获取当时线程类
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 判断是否初始化,假如没有则开始初始化
if (map != null) {
// 假如当时下标是方针元素,则回来,不然调用getEntryAfterMiss办法查找
// getEntryAfterMiss办法会继续查找并整理过期数据
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
// 初始化
private T setInitialValue() {
// 默许回来null,能够从头此办法决议每个线程在初始化map时获取的值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
....
return value;
}
// set办法
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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;
}
// 过期数据,需求整理
// replaceStaleEntry函数逻辑:向后查找,直到遇到方针元素更新数据或桶位为空
// 刺进值并铲除过期数据
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 最后刺进咱们的方针元素
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
三、内存走漏
强引证内存走漏
假如Entry的key为强引证,则会导致ThreadLocal实例在被创立它的线程毁掉时,而无法被回收,然后导致严峻的内存走漏问题,因而Eetry的key被声明为弱引证来防止这种问题
弱引证内存走漏
咱们知道每一个线程都存在一个ThreadLocalMap,Map中的key为一个ThreadLocal实例。并且key到ThreadLocal实例的引证为虚引证,也就是说当ThreadLocal置为null时,没有任何强引证指向ThreadLocal实例,所以ThreadLocal实例会被GC回收。但是value却不能被回收,由于存在一条从当时线程衔接过来的强引证(Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
)
针对上面的内存走漏问题,ThreadLocal在get和set时都会检测并铲除key为null的Entry,然后尽可能的防止内存走漏
运用主张
- 每次运用完ThreadLocal,都调用它的remove()办法,铲除数据
- 将ThreadLocal声明为private static,使它的生命周期与线程保持一致
四、总结
本文简略介绍了ThreadLocal相关的原理并剖析了部分首要源码,如有过错,还望不吝指正,感谢~