ThreadLocal详解
1、ThreadLocal是什么
从名字可以看出 thread 和local 直接翻译就是 线程 本地 也就是存在线程里面的变量 它适合线程相关联的。
在我们日常开发当中一定遇到过这样的问题,某些功能的实现需要一层套一层的方法调用,如果最后一层需要最开始一层的参数,就需要一层一层地传递过去。但是在很多时候一些中间层是不需要这些参数的,一层一层传递就显得比较臃肿。当然我们也可以吧这些参数设置成为应用类实体的变量,但是这个类大多数时候是单例的,设置成类实体的变量就需要考虑多线程并发问题。
threadLocal就是为解决这个问题而诞生的。为了解决上述问题,需要解决这样几个问题
1.在程序调用链上可以方便随时存取。
2.线程之间相互隔离,一个线程里面只能看到当前线程存的值,在其它的线程里面看不到。
3.需要能够避免内存泄漏,最好不用了能够自动删掉,要把用户当做智障来对待,万一用户使用不当不会造成严重的后果,用户存了忘记了删除不应该有内存泄漏导致整个系统崩掉了。
2、ThreadLocaL的实现
有人会说 实现上面几个要求还不简单,在Thread对象里面搞个Map不就行了,存取的时候拿到当前线程就可以操作这个map不就可以了。线程退出的时候这个map不就销毁了也不会有内存泄漏。
这个方法乍看起来是可行的,但是仔细琢磨下好像还是存在一些问题。
1、有些时候我们不是在线程的整个生命周期都需要使用到的存的这个对象的。直接存放在thread里面的map里面和线程的生命周期绑定了,有内存泄漏的风险。
2、线程池当中的线程是不会经常销毁的,而且会执行不同的代码逻辑,这就导致了可能在A方法当中错误地读到了B方法存放的值,AB两个方法本来是没有半毛钱关系的,只是不小心存放的键搞成一样了。
我们来看看ThreadLocal是如何实现的:
1、ThradLocal也和我们上面设计得一样 数据是存放在Thread里面的。在thread类里面有个threadLocals属性 这个属性就是要用来存数据的。比较有意思的是这个属性的类型是在ThreadLocal里面定义的,想想也应该是这样,Thread类本来是用来表示线程的,在1.0版本里面就有了,而ThreadLoal是1.2后面新增加的功能用来存数据的,秉着对扩展开发对修改封闭的原则 加功能不能过多的修改以前的代码,而且还是两个不同的功能放在一起当然不太好。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
2、接下来我们来看看具体存储结构是啥样的,ThreadLocalMap虽然写着是个map但是它里面存储数据的确是个数组,(毕竟map啥的也是在1.2版本才实现的)
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
当然ThreadLocalMap也不会直接遍历数据找值,而是自己实现了hash表,和HashMap采用的拉链发解决冲突不同 ThreadLocalMap使用的再hash,第一次取到的不是想要的值就再去一次。
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
再hash的方式也很简单 ,往后挪一个,如果越界了就到第一个
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
3、看起来和这个设计好像和我们刚刚自己设计的没多大区别啊,就是把代码移动到了一个新的类里面而已。其实不然,真正的精髓在存数据的这个Entry 上面
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry 继承了弱引用,每个entity都持有一个ThreadLocal对象的弱引用,ThreadLocalMap里面存的数据都是用ThreadLocal对象作为键的。
那弱引用有啥用呢,与弱引用对应的是强引用,我们平时用到的引用就是强引用,当一个对象没有强引用只有若引用的时候这个对象就会被回收。也就是说如果 ThreadLocal没有使用了那么它就会被回收掉Entity里面的引用的值就会变为空对象。
4、说了那么多,不用了就算变成空对象也没啥用啊entity还是存在,也不会被回收还是存在内存泄漏的风险。在get set各项操作当中都会有个清理的动作,如果Entity里面的ThreadLocal对象为空就是触发清理。
get第一次取到的值不是想要的那个接着往后取的时候就会触发
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
//如果ThreadLocal为nu就清理
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
set也是同样
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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;
}
//key 为空触发清理
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
3、总结
说了那么多了有些乱我们来理以理,看看ThreadLocal是如何实现我们提到的那几点的
①实际数据存储在Thread对象里面,实现了线程直接的隔离,达到了在调用链当中方便地随意取用的目的
②从Thread对象里面取值的时候使用的是ThreadLocal对象作为键,一个ThreadLocal只能存一个值,要存多个值就用多个ThreadLocal。这个就避免了我们的那个设计当中的线程池中完全不相干的代码当中不小心用了相同的键取到别人存的不相干的值的情况。
③弱引用的设计,使得存的对象在不使用的时候有个途径能被清理掉,避免了内存泄漏。