先来看看Jdk1.8对ThreadLocal的解释:
JDK1.8定义:当前类提供线程本地变量。这些变量不同于正常变量,而是访问这些变量的每一个线程,都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal通常是类中private static字段,期望与线程的某一个状态相关联(例如用户ID或事务ID)。
简单的来说,ThreadLocal是线程的本地变量,不用线程访问同一个ThreadLocal,得到的值是不同的。ThreadLocal保证了各个线程的数据互不打扰。
一个简单Demo
public class ThraedLocalDemo {
private static ThreadLocal local = new ThreadLocal();
public static void main(String[] args) {
local.set("test");
System.out.println(Thread.currentThread().getName()+"::"+local.get());
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"::"+local.get());
}
}).start();
}
}
//运行结果
main::test
Thread-0::null
当我们在main线程中进行set(),get()时可以获取到期望值。当我们在另外一个线程中尝试获取ThreadLocal中的值,得到的是null。
ThreadLocal源码
直接看get(),set()操作在源码中如何实现。
public T get() {
Thread t = Thread.currentThread(); //得到当前线程
ThreadLocalMap map = getMap(t); //从当前线程中获取ThreadLocalMap对象
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //以当前对象为key,获取entry
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value; //得到value
return result;
}
}
return setInitialValue(); //初始化方法。源码很简单,需要详细了解可以查看源码
}
public void set(T value) {
Thread t = Thread.currentThread(); //得到当前线程
ThreadLocalMap map = getMap(t); //得到当前线程的ThreadLocalMap对象
if (map != null)
map.set(this, value); 以当前对象为key,写入map中
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
从上面源码可以看出,无论 get() 或者 set() 方法,操作的都是当前线程 Thread
中的ThreadLocalMap
对象。
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null; //在Thread类中,维护的threadLocals
...
}
ThreadLocalMap
JDK1.8定义:ThreadLocalMap是一个定制的哈希映射,仅适用于维护线程本地值。ThreadLocalMap类是包私有的,允许在Thread类中声明字段。为了帮助处理非常大且长时间的使用,哈希表entry使用了对键的弱引用。有助于GC回收。
ThreadLocalMap
定义在ThreadLocal中的静态内部类,内部维护了一个Entry类和Entry[]。Entry的key是当前ThreadLocal
对象,value为ThreadLocal对象所对应的值。Entry[]存放当前线程中所有ThreadLocal变量。ThreadLocalMap内部实现如下图:
所以,我们继续了解ThreadLocalMap
代码是怎么实现的。
/*
ThreadLocalMap中除了主要的增删改查等操作外,
还有扩容等操作,因为原理和HashMap扩容大同小异,
需要的同学去查看源码吧,原谅我犯懒没有写。
*/
static class ThreadLocalMap {
/*
Entry并不同于HashMap,内部没有next指针指向下一个Entry,
也就是当发生Hash冲突时,不是以链表的形式解决冲突。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table; //存储所有ThreadLocal和value的Entry
private int size = 0;
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); //根据key的hashCode和长度得到索引位置
/*
从索引处开始遍历数组中所有Entry
依次判断当前位置的Entry.key是不是当前ThreadLocal,满足条件则进行覆盖,结束;
如果当前位置为空,则使用新的key,value进行覆盖,同时清理历史key=null的陈旧数据,结束;
否则将索引指向下一个位置,继续遍历。
同样的,在执行get(),remove()时也会使用这种方式进行遍历寻找。
*/
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果当前ThreadLocal对象==e.key,覆盖原有值
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size; //容量++
//如果超过阀值,就需要再哈希了
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
}
从上面代码可以发现,使用hashCode确定索引时,如果冲突严重时,会多次遍历整个数组,效率很低。
内存泄露
为什么ThreadLocal可能会导致内存泄露。
从上面代码可以知道,Entry类继承了WeakReference,value是以强引用的形式保存在Entry中,但是ThreadLocal是以弱引用的形式保存在WeakReference中。
这就导致了一个问题,ThreadLocal没有外部强引用时,发生GC时会被回收。如果创建的ThreadLocal一直运行下去,那么Entry中的value就不会被回收,发生内存泄露。
如何避免内存泄露
在调用ThreadLocal.get(),set()方法时,可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的Value就没有被引用,会被GC回收。当然,如果直接调用remove()方法,肯定会删除对应的Entry对象。
所以,要养成良好的编码习惯,使用完ThreadLocal之后,进行remove()操作。
ThreadLocal<String> tl = new ThreadLocal();
try {
localName.set("value");
// 其它业务逻辑
} finally {
localName.remove();
}