一、概念
ThreadLocal用来提供线程内部的局部变量,该变量在多线程情况下,可以提供线程隔离,使得局部变量仅在线程内部被访问到。
ThreadLocal的基本API:
- set(),设置当前线程绑定到局部变量
- get(),获取当前线程绑定的局部变量
- remove(),移除当前线程绑定的局部变量
二、内部原理
在JDK8以后,每个Thread内部都持有一个ThreadLocalMap,该Map的key为ThreadLocal本身,value为我们要与该线程绑定的局部变量。所以,当一个线程获取与自己绑定的局部变量时,只需要将ThreadLocal作为key传递给ThradLocalMap即可查到。我们申请一个ThreadLocal对象,只要其与不同的线程绑定,就可以达成针对不同线程,访问同一个ThreadLocal对象,但是数据是每个线程特有的。因为ThreadLocal只是作为key,每个线程自己持有的ThreadLocalMap是不同的,key相同,但value可能不一样。
三、核心方法源码
- set()方法:
public void set(T value){
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//获取当前线程持有的ThreadLocalMap
if(map != null){
//当前线程的map不为空,调用ThreadLocalMap的set方法,
//将自己(ThreadLocal)作为Key,value作为值存入Map中
map.set(this,value);
}else{
//如果当前线程没有ThreadLocalMap对象,则调用creatMap方法进行创建
creatMap(t,value);
}
}
ThreadLocalMap getMap(Thread t){
return t.thradLocals;
}
//当前线程的map为null时,为当前线程创建map,并初始化,将该ThreadLocal和value
//组成一个Entry存入。当set方法创建map时,value为我们传入set方法的value,
//当使用get方法创建时,value为initialValue()方法的返回值,默认为null
void createMap(Thread t, T firstValue){
t.threadLocals = new ThreadLocalMap(this,firstValue);
}
- get()方法:
public T get(){
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);
if(map != null){
//取值时,将ThreadLocal作为key传入,去map中查询有无对应的value
ThreadLocalMap.Entry e = map.getEntry(this);
if(e != null){
//查询到的节点(即Entry,因为map实际上是一个Entry数组,
//每个Entry包含一个Key和一个Value)
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//当map不存在或者map存在但是要没有当前ThreadLocal作为key所关联的Entry时
//调用该方法为当前线程创建map或者为该ThreadLocal创建一个Entry,
//并将value设置为null(默认为null),最后将Entry插入到map中
return setInitialValue();
}
private T setInitialValue(){
//默认返回为null,用户可以重写该方法,相当于为一个没有与我们想要的局部变量绑定的ThreadLocal设置初值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if(map != null){
map.set(this,value);
}else{
//如果当前线程没有ThreadLocalMap对象,则调用creatMap方法进行创建
creatMap(t,value);
}
return value;//返回我们为该ThreadLocal设置的默认值
}
//为当前ThreadLocal的分配初值,以便组成Entry
protected T initialValue(){
return null;
}
- remove()方法:
public void remove(){
ThreadLocalMap map = getMap(Thread.currentThread());
if(map != null){
//当threadLocalMap对象不为空时,将该ThreadLocal作为key的Entry从数组中删除
map.remove(this);
}
}
四、ThreadLocalMap
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,而是独立实现了Map的功能,内部的Entry也是独立实现的。
根据类图,我们可以得知,Entry的Key继承了弱引用类。所以,在第二部分的图中,ThreadLocalMap中的Entry是通过弱引用指向ThreadLocal的,而用户通过一个强引用指向ThreadLocal,然后就可以将该ThreadLocal传入到线程的map中来获取值或者存值。
ThreadLocalMap的初始容量默认为16,同时持有一个int类型的size变量(默认为0),用来记录当前ThreadLocalMap中含有的元素(Entry)个数,同时还有一个int类型的threshold用来记录进行扩容的阈值(当数组中元素的个数大于该值,数组进行扩容,默认为0)。
成员变量:
/**
* 初始容量 - 必须是2的整次幂
**/
private static final int INITIAL_CAPACITY = 16;
/**
*存放数据的table ,Entry类的定义在下面分析,同样,数组的长度必须是2的整次幂
**/
private Entry[] table;
/**
*数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值
**/
private int size = 0;
/**
*进行扩容的阈值,表使用量大于它的时候进行扩容
**/
private int threshold; // Default to 0
- set()方法:
private void set(ThreadLocal<?> key,Object value){
//取得当前线程维护的ThreadLocalMap中的Entry数组。
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
//计算索引
int i = key.threadLocalHashCode & (len-1);
//使用线性探测法解决hash冲突
for(ThreadLocal.ThreadLocalMap.Entry e = tab[i];e!=null;e=tab[i = nexIndex(i,len)]){
ThreadLocal<?> k = e.get();
//如果ThreadLocal对应的Entry以前有值,直接覆盖
if(k == key){
e.value = value;
return;
}
//如果key为null,即原来该ThreadLocal所对应的Entry没有被回收,
//但是ThreadLocal被回收了
if(k == null){
//此时,用新元素替代旧元素
replaceStaleEntry(key,value,i);
return;
}
}
//如果数组中不存在ThreadLocal所对应的Entry,则直接插入即可
tab[i] = new Entry(key,value);
int sz = ++size;
/*如果没有key为null的元素而且Entry数组的大小大于等于了阈值,进行扩容
*/
if(!cleanSomeSlots(i,sz) && sz >= threshold)
rehash();
}
private static int nextIndex(int i, int len){
return ((i+1 < len) ? i+1 : 0);
}
/*
将key为null的Entry的value也设置为null
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
在这里,index的计算使用了hashCode & (len - 1).此处这样的处理是和HashMap中index的计算原理相同的。
五、Entry定义:
static class Entry extends weakReference<ThreadLocal<?>>{
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);//设置key的引用类型为弱引用
value = v;
}
}
为什么要将ThreadLocalMap中Entry持有的key的引用类型设置为弱引用呢?
这是因为如果我们使用强引用的话,当我们用户使用完ThreadLocal对象后,我们用户持有的指向ThreadLocal的引用就被回收掉了,此时逻辑上来说应该把堆上ThreadLocal对象也给回收掉,但是此时ThreadLocalMap中还有一个Entry的Key持有一个指向ThreadLocal的强引用,导致ThreadLocal无法被JVM回收。造成内存泄漏。而使用弱引用的话,当JVM垃圾回收时,弱引用是不管内存是否充足,都会被直接回收的。所以,当我们用户持有的强引用被回收后,则下次垃圾回收的时候,就会把我们创建的ThreadLocal对象回收掉(此时,只有一个Entry的弱引用指向它),但是此时还是会存在内存泄漏问题。
这是因为,虽然ThreadLocal对象被回收了,但是此时Entry对象还没有被回收,还是被线程中的ThreadLocalMap存着一个指向它的强引用,此时Entry的状态是key为null,value指向我们存入的value,因此这个Entry永远不会被访问到了,但它也没法被回收,所以还是存在内存泄漏问题。使用弱引用只是解决了ThreadLocal造成的内存泄漏问题,但依然存在着Entry的内存泄漏问题。
如何解决呢?每次使用完一个线程局部变量,就是用remove()方法将其从ThreadLocalMap中移除。(当然如果线程运行完毕,则也不会产生内存泄漏,因为ThreadLocalMap也被回收掉了)
除此之外,如果下次我们还会使用ThreadLocalMap的get或者set方法,此时ThreadLocalMap会对key为null的Entry进行清除(前面第四部分的源码可以得知这点)。所以,使用弱引用会保证当一个ThreadLocal不会再被使用后,ThreadLocal一定会被回收,其Entry会在下次调用ThreadLocalMap的get、set、remove方法时清除,避免内存泄漏。(但是,当我们一个线程只是用一次ThreadLocal,后面线程一直运行,但不会再次使用ThreadLocal的情况会造成内存泄漏,所以最好还是用完就进行remove)。