(关于 ThreadLocal 的网上应该有很多优秀的文章,本文用于个人整理,以及有需要的人拿来参考,有什么不正确的地方,欢迎指正,共同进步。)
ThreadLocal 通常被称为本地线程变量,为什么呢?因为在多线程环境下,通过ThreadLocal,我们只要使用一个ThreadLocal对象,就可以为每个线程都可以维护一个该对象的副本,而且每个线程都可以单独修改自己的副本,并且不会影响到其他线程的副本,那么ThreadLocal是怎么实现这个强大的功能的呢?
我们来一起探索一下:
ThreadLocal主要的方法和内部类:
1、set 方法;
2、get 方法;
3、remove 方法
4、ThreadLocalMap:维护键值对(以 threadLocal 本身为键,set 方法中的参数值为 value),不太明白的话,请看后文。
我们来具体看看这几个方法:
1、set 方法;
ThreadLocal<String> threadLocal = new ThreadLocal<String>();
threadLocal.set("ss");
代码很简单,第一行新建一个 threadLocal 对象,第二行将字符串“ss” set到 threadLocal 对象中,然后结果呢?结果就是 threadLocal 为当前线程维护了该对象的独立的副本,下面我们来看一下源代码
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//通过当前线程找到相应的 map
ThreadLocalMap map = getMap(t);
if (map != null)//map如果不为空,也就是之前已经进行过set操作
map.set(this, value);//以当前 threadLocal 对象为键,set 方法的入参为值放到 map中,并取代旧值
else//map 为空,则新建一个 map
createMap(t, value);
}
获取当前线程没有问题,下一行 getMap(t),是如何获取的呢?为什么通过 当前线程可以获取到 threadLocal 中的静态内部类 ThreadLocalMap对象呢?
原来在 线程类Thread中将它作为 Thread 的一个属性了!这个是 ThreadLocal 实现本地线程变量的关键!!!通过这个getMap(Thread t)方法,对于每个线程都可以得到不一样的ThreadLocalMap。public class Thread implements Runnable {
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
我们再来关注 map.set(this, value); 这一行,很明显,我们可以看到这个 ThreadLocalMap 是以当前的 threadLocal 对象作为key,以set的入参值作为 value进行设置,分析到这里了,我们就进到这个 map.set(this, value); 方法内部来仔细解析一下吧。
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;//获取当前 ThreadLocalMap 的 Entry 数组
int len = tab.length;//获取 Entry 数组的长度
int i = key.threadLocalHashCode & (len-1);//根据 key 的 hashcode 计算出 数组下标 与 HashMap 类似
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {//这里有点不解,为什么要循环 Entry 数组
ThreadLocal k = e.get();
if (k == key) {//覆盖旧值,并结束
e.value = value;
return;
}
if (k == null) {//为什么key会为null值呢?我们在remove方法介绍的时候将会叙述
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();//重新计算hash算法
}
2、get 方法
threadLocal.get()
太简单了,这一句就可以获取到当前线程的threadLocal维护的副本,但是里面的实现是怎样的呢?我们来看一下源代码:
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//通过当前线程找到相应的 map
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
其实看懂了上面的 set方法,这个 get 方法也是蛮简单的了,获取当前线程——>获取当前线程的 ThreadLocalMap——>返回值或者进行初始化工作,我们就不展开叙述了。
3、remove 方法
这里我们必须强调的一点就是,当你使用完threadLocal对象时,必须要调用remove方法,不然后果蛮严重,为什么呢?我们来具体看一下源代码
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
前面几行没有什么疑问,最后一行调用ThreadLocalMap 的remove(this)方法,将当前线程维护的副本清空,那为什么一定要remove呢?我们来看一下这个ThreadLocalMap静态内部类的结构
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal> {//将 ThreadLocal 虚引用作为 key 当发生GC的时候会对key进行回收,但是不回收 value
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
...
}
将 ThreadLocal 弱引用作为 key 当发生GC的时候会对key进行回收,但是不回收 value,也就是说就发生GC的时候,只是对key进行了垃圾回收处理,对value不管不问,然后一直在内存中保留着,导致了内存溢出,因此在上面的set方法中,会有 if (k == null)这个判断(可能发生了 key 回收,导致 key 为 null)以及后续处理。
为什么弱引用会在GC的时候被回收,其实引用分为以下几种,我们简单来介绍一下:
1、强引用:我们平时接触到的基本都是强引用,eg:Object object = new Object(); 或者String string = new String(); 中 object 和 string都是强引用,就算是虚拟机内存不够他们的空间也不会被回收,而是抛出 OutOfMemoryError 的错误;
2、软引用:可有可无,但是在没有内存的时候才会进行回收,否则一直保留在内存中;
3、弱引用:可有可无,但是比软引用拥有更加短的生命周期,在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会将它回收;
4、虚引用:形同虚设,虚引用主要用来跟踪对象被垃圾回收的活动。