深入理解ThreadLocal应用

ThreadLocal

ThreadLocal类用来提供线程内部的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量的传递的复杂度。说白了,ThreadLocal就是创建了能够拿到以自己为key存在当前线程的内容,来达到对当前线程中的所有方法共享,减少了参数的多层传递。
这么说可能不明显,我来举个例子吧:

@Service
public class CommonService {

    public void toWork(User user){
        bus(user);
    }

    public void bus(User user){
        metro(user);
    }

    public void metro(User user){
        System.out.println("到达公司");
    }
}

比如一个接口叫工作,需要调用去工作的方法,而去工作不管是公交还是地铁都需要个人信息。传统做法就是如上代码,将user信息传递下去。这和ThreadLocal有什么关系呢?下面来看:

@Service
public class CommonService {
    private ThreadLocal<User> local = new ThreadLocal<>();

    public void toWork(User user){
        local.set(user);
        bus();
    }

    public void bus(){
        User user = local.get();
        metro();
    }

    public void metro(){
        User user = local.get();
        System.out.println("到达公司");
    }
}

这样在该类中,user都是可以共享的,如果要跨类,就得将local移动到工具类中,给不同的类调用,我就不多加写了。特别注意,一个线程对同一个threadlocal来说只能塞入一个value,再次塞入就会更新之前塞入的值。

到这里就会有人问了,为什么不将user定义成全部变量呢?
你想想,每个请求的人都不一样,如何定义全局变量。

那ThreadLocal又是如何做到让一个local变量就set进去就能拿到不同用户访问时的用户信息呢?
这就得从源码来解释了。

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();
 }
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 getMap(Thread t) {
        return t.threadLocals;
}

每一个请求其实就是一个线程,不管是get还是set都是操作当前线程的threadLocals变量,也就是说和当前线程threadLocals起到了全局变量的作用,但只对当前线程有用。
下面来详细讲讲线程存储的ThreadLocalMap吧:

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;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
}

每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647。

在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下:
1、如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上;
2、不巧,位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value;
3、很不巧,位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置;

这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置

可以发现,set和get如果冲突严重的话,效率很低,因为ThreadLoalMap是Thread的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。

这里需要注意的是,ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。

内存泄漏

通过之前的分析已经知道,当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

但是内存泄漏是否真的会发生呢?
可以查看ThreadLocalMap的getEntry(),getEntryAfterMiss(ThreadLocal

题外话:

其实shiro保存subject信息就是通过ThreadLocal来保存,然后通过SecurityUtils.getSubject()给不同层级的方法可以拿到subject,不过shiro特别之处就是用了InheritableThreadLocal。为什么用这个呢?

InheritableThreadLocal 可以让用户自行 new Thread  出来的线程可以获取到 Subject,否则用户还要额外想办法怎么获取到这个 Subject。通俗的说就是,当你启动多线程的时候,如果用ThreadLocal将拿不到主线程的subject,这个就是解决了这个问题。配置化用InheritableThreadLocal就很好用。

猜你喜欢

转载自blog.csdn.net/hjucook1988/article/details/81431533