ThreadLocal 的定义及使用:
ThreadLocal类 位于 java.lang 包下
ThreadLocal, 翻译成中文,应该叫 线程局部变量。作用是存储线程本地变量,意思是说,在ThreadLocal中保存的变量,都归当前线程所有,不同线程之间相互隔离,互不影响,避免和主内存通信,从而提高效率。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
先看一下使用效果:
public class TestThreadLocal {
private static ThreadLocal<String> tl = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
tl.set("thread-1 set String");
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(tl.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
System.out.println(tl.get();
}).start();
}
}
以上代码中,定义了两个线程,线程1 启动后,往ThreadLocal中设置了一个字符串,然后休眠3秒钟,在这之间,线程2 往ThreadLocal中去获取值并输出,3秒钟后,线程1再次获取ThreadLocal中的值并输出。
执行结果:
null
thread-1 set String
可以看出,在线程1往ThreadLocal中保存了一个String字符串后,线程2获取结果为null,说明线程2并不能得到线程1所保存的数据,而线程1本身则可以拿到。由此可以得出答案:
各个线程在ThreadLocal中保存的数据,只对当前线程可见,多个线程对ThreadLocal的操作,其实就是操作各自线程中的数据。
ThreadLocal 的原理:
既然ThreadLocal可以做到线程隔离,那么他是怎么做到的呢?说到原理,就一定离不开源码。首先我们就要看看JDK源码对ThreadLocal这个类的定义。在对ThreadLocal的操作中,我们着重看set()和get()这两个函数。暂时忽略类中的其他属性和方法。
先看以下ThreadLocal中的set()函数,该函数是往ThreadLocal中保存数据。
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,通过ThreadLocalMap保存数据。
如果没有获取到ThreadLocalMap,就新建一个ThreadLocalMap,然后通过ThreadLocalMap保存数据。
我们发现,在这个函数中,涉及到了一个叫ThreadLocalMap的类,这是个什么类,我们看一下,点进去发现该类是ThreadLocal的内部类。
static class ThreadLocalMap {
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold;
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
先不看类中的其他方法,只看定义的变量和内部类。得出以下结果:
ThreadLocalMap 是 ThreadLocal 的静态内部类。
ThreadLocalMap 中定义了一个Entry[] table ,而Entry 是他的内部类。
Entry类中定义了一个 Object value 变量,这个value就是我们存入的数据。后面再分析。
类的定义先看到这里,我们大致可以猜到,我们往ThreadLocal中保存的变量,最终是将一个Entry对象,存到了ThreadLocalMap的table数组中, 而Entry对象的value属性,就是我们要存入的数据。我们带着疑问,继续往下看。
这里返回到上面的set()方法,我们看到set()方法中,是通过getMap(t) 来获取ThreadLocalMap的,参数 t 代表当前线程。接下来我们看一下getMap(t)这个函数。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到,在获取ThreadMap的时候,他返回的是t.threadLocals 这个变量, 而 t 是调用该方法时所传入的参数,即:当前线程。它是Thread类型,说明在Thread类中有一个变量 threadLocals。 看到这里,我们不用多想,肯定要到Thread类中一看究竟。
我们到Thread类中可以看到,在Thread类中,定义了这样一个变量:
public class Thread implements Runnable {
// 省略其他定义
...
...
ThreadLocal.ThreadLocalMap threadLocals = null;
}
看到这里,ThreadLocal为什么线程隔离的原因,大概就是弄明白了。分析原因如下:
我们在使用ThreadLocal保存数据的时候,其实是将数据保存在了Entry[]中。
该 Entry[] 是在ThreadLocalMap中定义的一个变量 tables
既然tables是一个数组,说明我们可以保存多个数据。
Entry 是 ThreadLocalMap的内部类,我们要保存的每一个数据就是会转化成Entry的方式保存。
归根结底,我们的数据是保存在ThreadLocalMap这个类的一个变量 tables 中。
而ThreadLocalMap同时又是Thread类中的一个变量 threadLocals
因为每一个Thread实例代表着一个线程,所以我们保存在Thread实例中的数据,当然是各个线程各自维护的。
我们已经知道,要保存的变量是以Entry的方式保存,那么到底是怎样转化为Entry的呢?接下来我们来看看。
我们回到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);
}
可以看到,在保存数据的时候,调用了map.set(this, value), 其中this代表当前ThreadLocal实例, value代表要保存的变量。我们看一下这个函数。该函数是ThreadLocalMap中的一个函数。
private void set(ThreadLocal<?> key, Object value) {
// 先获取之前的数组,这个 table 就是ThreadLocalMap中的tale 变量
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();
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();
}
上面的代码,大致就是获取原来的table数组,然后计算要添加的数据应该保存在数组的哪个位置,最后new 一个Entry实例,保存到该位置上去。在上面代码中可以找到这行代码:
tab[i] = new Entry(key, value);
这个key 就是传入的 当前ThreadLocal 的实例对象, value是要保存的变量数据。
我们再看一下Entry类的定义及该类的构造方法。
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
最终,我们要保存的数据,就是Entry中的value。
最后,简单总结:
调用 ThreadLocal.set()保存数据时,实际是将数据 以Entry的形式 保存到 当前Thread实例中的 threadLocals 变量中的 tables 数组中。由于Thread代表线程实例,所以各个线程所保存的数据互不可见,互不影响,只由各自的线程独自进行维护。
细心的人可能会发现,Entry类 继承了一个 WeakReference<ThreadLocal<?>>的 弱引用类型
为什么Entry的key 是弱引用(WeakReference)类型的ThreadLocal实例?
首先我们需要了解Java中的强、软、弱、虚这四种引用。这里不再做详细说明。而其中弱引用就是表示: 只要下次垃圾回收启动时,就回收当前实例。
试想一下,如果这里不使用弱引用而使用强引用,实质上就会造成该Entry与当前线程的生命周期绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点,但是很有可能该Entry已经不会再被使用到。这就可能导致内存泄漏问题。
当然了,即使这里Entry中的key为弱引用,也并不保证不会发生内存泄漏问题,因为Entry中的value仍然是强引用。即使在垃圾回收时,key被回收,这时该Entry的 key == null, 但是仍然有一个value是强引用状态而导致该Entry不能被回收。
怎样防止内存泄漏
最保险的方式,是在每次使用了ThreadLocal时,调用ThreadLocal中的**remove()**方法,即可避免出现内存泄漏问题。
对remove()源码的分析,参考:ThreadLocal源码分析:(三)remove()方法