点赞再看,养成习惯,大家好,我是辰兮!今天介绍ThreadLocal底层实现原理。
目录
前言
现在是下午4点,王二狗已经提前把工作已经干完了,在和女神如花闲聊的过程中逛了逛XXX网站,突然发现部门一位大佬发布的文章,里面讲着多线程、线程变量以及ThreadLocal原理等等,王二狗此时对ThreadLocal非常感兴趣,对女神说:“宝贝儿,你等会,我要去干一件大事!”,接下来王二狗就开始了研究之路。。。
如花:“活该你单身!!!”
一、ThreadLocal的使用
ThreadLocal<String> local = new ThreadLocal();
local.set("userName");
String userName = localName.get();
localName.remove();
使用其实很简单,线程进来之后初始化一个ThreadLocal,然后通过set()方法设置想要存的值,然后在调用remove()方法之前使用get()方法即可获取到之前set设置的值。
它可以做到数据隔离,也就是说线程1使用get()方法获取不到线程2的值。(也不完全能做到)
为什么呢?话不多说,接下来我们来探寻一下原因!
二、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);
}
可以看到他首先调用了Thread.currentThread()获取当先线程
然后调用了getMap()获取到线程对象,后面就是为线程对象set指定的值
可以发现set()源码非常简单,主要是ThreadLocalMap需要我们注意
话不多说,咱们点进去看看!
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到它调用了Thread的threadLocals对象:
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
。。。
}
到这里我们基本可以知道ThreadLocal数据隔离的真相了,每个线程Thread都维护了自己的threadLocals变量,所以每次使用ThreadLocal.get()时都是从自己线程里面拿到自己的threadLocals变量,拿不到别人的变量,从而实现了数据隔离。
源码可以看出threadLocals是ThreadLocal里面的ThreadLocalMap,那么ThreadLocalMap底层结构长什么样呢?
话不多说,我们接着看!
三、ThreadLocalMap底层结构
这张图可以看出ThreadLocalMap并未实现Map接口,而且它的Entry继承了WeakReference(弱引用),也没看到HashMap的next变量,所以也不是一个链表
那用什么来存储数据呢?数组?
没错! 就是用的数组来存
在开发过程中,一个线程Thread肯定会有多个ThreadLocal来存放不同的对象,但是这些对象都会放到Thread.threadLocals里,所以用到了数组来存
那么用了数组怎么解决Hash冲突问题呢?
还是一样,看源码来找答案:
static class ThreadLocalMap {
private void set(ThreadLocal<?> key, Object value) {
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();
}
}
可以看到ThreadLocalMap.set(T t)方法的第五行:
int i = key.threadLocalHashCode & (len-1);
然后会判断如果k==key则将value值直接赋值给索引i上:
if (k == key) {
e.value = value;
return;
}
如果k==null则初始化一个Entry对象放到索引i上面:
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
如果索引i的位置不为空并且key不等于Entry则去找下一个位置,直到为空为止:
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
所以在get的时候,也会根据ThreadLocal对象的hash值,定位到指定位置,然后判断该位置key和Entry对象是否和get的key一样,如果不一样,则判断下一个索引位置
所以set和get如果冲突很严重的话,效率还是很低的!
下面来看一下get源码
四、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();
}
可以看到ThreadLocal的get()源码也很简单,如果调用返回值不为null,则返回它的value值,如果为空则执行设置初始值setInitiaValue()方法,这里主要看ThreadLocalMap的getEntry()方法
话不多说,咱接着看!
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);
}
可以看到,getEntry()的实现逻辑就是拿到key的hash值,然后判断此处是否是否等于key,如果是则返回这个Entry对象,如果不是则执行getEntryAfterMiss()方法。
好的,接下来我们再来看一下getEntryAfterMiss()源码:
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;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
简单的来说,getEntryAfterMiss()方法就是一直往下查找,直到找到对应的位置。
总结来说:ThreadLocal.get()方法就是获取到Enry的i值,如果等于key则返回,如果不等于key则一直查找,直到找到对应的位置。
说了这么多,不知道大家有没有一个疑问?
上面说了ThreadLocalMap.Entry是继承了WeekReference(弱引用),那为什么呢?
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到Entry继承了弱引用并且泛型参数是ThreadLocal<?>,所以参数ThreadLocal<?>也是一个弱引用
接下来介绍一下为什么要使用弱引用:
五、为什么使用弱引用
在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
此处引用:弱引用和其他引用的区别
用弱引用的话,当没有强引用来引用ThreadLocal实例时GC就会回收ThreadLocalMap中Entry的key(ThreadLocal),但是Entry的value变量(Object value)是一个强引用,则只要ThreadLocalMap存在则value就不会被回收,ThreadLocalMap又是Thread的成员变量,所以value会一直保留在线程被销毁才会回收,这里就会发生内存泄漏!!!
既然会发生内存泄漏,那为什么要设计成弱引用呢?
来假设我们如果使用强引用,则ThreadLocalMap所有数据都与Thread生命周期绑定,这样就很容易出现线程持续活跃导致线程一直不被回收,这样Entry的key(ThreadLocal)以及value都不会被回收,还会发生内存泄漏o(╥﹏╥)o
那么,怎样才不会发生内存泄漏?
当采用弱引用时,key是弱引用则当GC触发时,key就会被回收,所以会出现一些key(ThreadLocal)值为null但value值不为null的情况
这些Entry如果不主动清理,就会一直停留在ThreadLocalMap中。所以官方在ThreadLocal中get()、set()、remove()这些方法中都存在清理ThreadLocalMap实例key为null的代码。所以在使用ThreadLocal时,对于用不到的对象,要及时使用remove()方法清除,不然就会导致内存泄漏!!
在代码中如何实现每次用完ThreadLocal后使用remove()方法清除呢?
介绍一种在SpringBoot项目中使用的方法(如果是其他项目可以参照):
继承HandlerInterceptor类,实现afterCompletion()方法,此方法的作用是在每一次请求结束后都会执行方法内的代码。
此时我们将remove()方法写在afterCompletion()方法内即可!
public class UserCenterInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ThreadLocalUtils.removeUser();
}
总结
总的来说ThreadLocal就那么几个方法,很少,但是在研究源码的过程中,发现每一个方法都非常的精妙,特别是为什么要继承弱引用,在细节的处理往往可以看出我们和大佬之间的区别,我之前觉得不合理的地方往往在弄懂之后才能发现是多么的巧妙!
最后:
我是辰兮,不开心就笑一笑,世界没有那么糟,咱们下期见!
靓仔们的 【三连】 就是辰兮创作的最大动力,如果本篇博客有任何错误和建议,欢迎靓仔们留言!