点赞再看,养成习惯,大家好,我是辰兮!今天介绍ThreadLocal底层实现原理。

目录

前言

一、ThreadLocal的使用

二、set()源码

三、ThreadLocalMap底层结构

四、get()源码

五、为什么使用弱引用

总结


前言

现在是下午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就那么几个方法,很少,但是在研究源码的过程中,发现每一个方法都非常的精妙,特别是为什么要继承弱引用,在细节的处理往往可以看出我们和大佬之间的区别,我之前觉得不合理的地方往往在弄懂之后才能发现是多么的巧妙!

最后:

我是辰兮,不开心就笑一笑,世界没有那么糟,咱们下期见!

靓仔们的 【三连】 就是辰兮创作的最大动力,如果本篇博客有任何错误和建议,欢迎靓仔们留言!

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐