你们有没有经历过这种痛苦:很多方法一层一层调用,每次调用都要传相同的参数。这个时候就可以使用ThreadLocal来解决你的烦恼。
ThreadLocal是什么?
ThreadLocal表示的是线程本地变量,也可以说是每个线程都有同一个变量的独有拷贝。这个听上去可能不是那么好理解,我们先来看一下它能是怎么用的。
ThreadLocal的使用
在ThreadLocal中,我们经常使用的就下面三个方法:
public void set(T value)
public T get()
public void remove()
这三个方法也很简单,见名思意,下面看看它的具体用法
public class ThreadLocalDemo {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws Exception{
new Thread(()->{
threadLocal.set("123");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+","+threadLocal.get());
}).start();
threadLocal.set("abc");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+","+threadLocal.get());
}
}
// 输出
main,abc
Thread-0,123
用法也很简单,有点类似HashMap,区别在于它的设置和取值都没有用到key。
看到上面这么多,大家可能感觉ThreadLocal也没啥难的,其实,它还是有点东西的。当谈到ThreadLocal,基本都会谈到它的内存泄漏还有它的弱引用。下面我们就来仔细分析一下ThreadLocal原理和它出现内存泄漏的原因。
ThreadLocal原理
首先,我们来看一下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,这个Map就是用来保存我们需要set的值,但是,ThreadLocalMap只是ThreadLocal中的静态内部类,而且获取的ThreadLocalMap并不是定义在ThreadLocal中的属性,而是定义在Thread类中的属性。简单来说:ThreadLocal相当于一个工具类,用它可以获取Thread中的ThreadLocalMap属性,然后将值存入ThreadLocalMap中。
第二,在set方法中会调用expungeStaleEntry(int staleSlot)
方法,这个方法用来清除key为null的键值对,不仅在set方法中用到,在其他方法如get()和remove()都会用到。为什么这么做呢,这个和后面内存泄漏一起说。
接下来,我们看一下get方法的源码
public T get() {
Thread t = Thread.currentThread();
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;
}
}
return setInitialValue();
}
这个方法也很简单,获取到ThreadLocalMap,然后通过this,也就是ThreadLocal来获取value。
通过上面两个方法,我们知道,ThreadLocal相当于工具类,用它来操作Thread中的ThreadLocalMap。那这三者是什么关系呢?我们用下面这个图来表示一下:
上图中有一条虚线,它表示弱引用,这就和内存泄漏有关系了
内存泄漏
在提到内存泄漏,很多人会联想到内存溢出,这里先解释一下两者分别是什么意思:
- 内存泄漏:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
- 内存溢出:没内存可以分配给新的对象了。
在ThreadLocal中,为什么会出现内存泄漏呢?
上面我们提到,ThreadLocalMap中会存在ThreadLocal的弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
这是ThreadLocalMap中Entry的定义,它继承自弱引用,里面key就是ThreadLocal弱引用。
为什么ThreadLocal要这么设计呢?
首先,我们要知道,每个线程都会有一个ThreadLocalMap,如果线程一直存活,那ThreadLocalMap中存的键值对会越来越多。如果将里面key设计成弱引用,当ThreadLocal不存在时,gc会自动回收对应的key,因为ThreadLocal这个工具类都不存在了,那里面存的key和value也自然取不到了,自然可以回收了。这时候gc只会回收key,而对应的value还是存在的,这就有可能造成内存泄漏了。
那value要怎么删除呢?
上面我们提到一个方法expungeStaleEntry(int staleSlot)
,这就是用来处理key为null的value。因此当我们调用set,get和remove方法后,ThreadLocal会自动帮我们删除key为null的value,这就有效降低了内存泄漏的出现。但是,如果gc回收key以后,我们没有使用那些方法,那岂不是还会出现key为null的value?这就需要我们平时开发的时候注意,使用完ThreadLocal后,手动调用remove,保证value也会被回收。
总结
ThreadLocal可以用来操作每个线程特有的ThreadLocalMap,用于资源的线程隔离。也可以用作单个线程在各个时间点的资源共享,解决方法之间重复参数的传递。