1.什么是ThreadLocal
线程内部的数据存储类,通过它可以在指定的线程中存储数据,对数据存储后,只有在线程中才可以获取到存储的数据,对于其他线程来说是无法获取到数据。
- 每个线程线程内部都有一个Map。
ThreadLocal.ThreadLocalMap threadLocals = null;
- Map里面存储线程本地对象(键)和线程的变量副本(值)
- 但是,线程内部的Map是由ThreadLocal中维护的,由ThreadLocal的负责向Map获取和设置线程的变量值。
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
2.ThreadLocal核心方法及源码
public T get() //用于获取当前线程的副本变量值。
public void set(T value) //用于保存当前线程的副本变量值。
public void remove() //删除当前前程的副本变量值。
2.1 get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //得到当前线程的map(threadLocals)
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//这里this代表的是当前ThreadLocal
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
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;
}
protected T initialValue() {
return null;
}
步骤:
- 获取当前线程的ThreadLocalMap对象threadLocals
- 从map中获取线程存储的KV Entry节点
- 从入口节点获取存储的值副本值返回
- map为空的话返回初始值null,即线程变量副本为空。
2.2 set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
-
获取当前线程的成员变量map
-
map非空,则重新将ThreadLocal和新的价值副本放入到map中
-
map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal的和值副本放入map中。
2.3 remove方法
public void remove() {
ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private void remove(ThreadLocal<?> key) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
-
获取当前线程的成员变量map
-
map非空,则删除在map中键值
3. ThreadLocalMap
ThreadLocalMap是ThreadLocal中的内部类,是一个特别的map没有实现map接口,用独立的方式实现了map的功能,其内部的入口也独立实现。
在ThreadLocalMap中,也是用输入来保存KV结构数据的。但是进入中关键只能是ThreadLocal的对象,这点被输入的构造方法已经限定死了。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
继承自WeakReference的(弱引用,生命周期只能存活到下次GC前,在《垃圾回收机制(GC)》一文中有说明),
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value
永远无法回收,造成内存泄露。
ThreadLocalMap设计时的对上面问题的对策,getEntry函数的流程大概为:
- 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
- 如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry。否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
4.解决哈希冲突
和HashMap中的最大的不同在于,ThreadLocalMap结构非常简单,没有下一个引用,也就是说ThreadLocalMap中解决哈希冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始密钥的哈希码值确定元素在表数组中的位置,如果发现这个位置上已经有其他关键值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决散列冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
显然ThreadLocalMap采用线性探测的方式解决哈希冲突的效率很低,如果有大量不同的ThreadLocal的对象放入地图中时发送冲突,或者发生二次冲突,则效率很低。所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到地图中的重点都是相同的ThreadLocal的,如果一个线程要保存多个变量,就需要创建多个ThreadLocal中,多个ThreadLocal中放入地图中时会极大的增加哈希冲突的可能。