ThreadLocal及InheritableThreadLocal基本原理及注意项

背景

        同事在使用ThreadLocal时,需要把主线程中的ThreadLocal值传输到子线程(线程池维护)中,故使用了InheritableThreadLocal作为传输。后发现,主线程执行ThreadLocal.remove()后,子线程中的ThreadLocal并不会被remove(),导致线程池中维护的ThreadLocal存储的值一直不变。于是深入进行了研究。

ThreadLocal原理

        要讲清楚InheritableThreadLocal原理,首先要知道ThreadLocal是如何做到在线程中存储变量的,因为两者本质上是一个东西,只是InheritableThreadLocal多了一个子线程可继承的功能(部分方法实现不同)。

        直觉上理解可能认为ThreadLocal是一个Map,保存着Thread和存储变量的一个映射,就像这样Map<Thread, V>,但实际不是。

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

       ThreadLocalMap中里面有一个Entry类,类似我们熟知的HashMap中,也有一个Entry类一样,ThreadLocalMap中的Entry存的则是<ThreadLocal<?>, value>,保存着ThreadLocal和value的映射。

static class ThreadLocalMap {
...
    static class Entry extends WeakReference<ThreadLocal<?>> {
        
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
        }
    }
...
       
    private Entry[] table;
...
}

        同时ThreadLocalMap中,有一个Entry数组table作为存储。那么我们可以画出实际上Thread和ThreadLocal的关系实际上是这样的。

        

         也就是一个Thread可以拥有多个ThreadLocal,通过ThreadLocalMap来进行存储,这就是ThreadLocal和Thread之间的关系。

        顺带补充一下,我们知道HashMap底层结构是数组+链表,用Key值的HashCode来计算数组中存放的index,用链表来解决哈希冲突,即链地址法。

        而ThreadLocalMap类似,利用ThreadLocal的hashcode来计算数组中存放的index,但是解决哈希冲突方法是,如果计算出来的index已存放了元素,那么index+1,越界时则重新回到0。当然这样最终会导致无地址可以用,再一直这样循环查找会导致死循环,所以当数组中元素超出一定阈值(threshold)时,就会进行扩容,此处便不再进行展开。

        前面讲了ThreadLocal和Thread的联系,那么调用ThreadLocal.set的时候,是如何做到保存到对应线程中的,调用get时候又是怎么取出来的。

Set方法

        首先先看到 ThreadLocal的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);
    }

    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

         当调用set时,首先会获取到当前执行线程,并获取当前线程的 threadLocals 属性,即上文我们着重讲的ThreadLocalMap,若不存在则创建,然后以当前的ThreadLocal对象作为key,存入ThreadLocalMap中。大致流程图如下:

        

         这里我们也可以知道Thread中的ThreadLocalMap并不是构建出来就创建了的,而是需要用到的时候再去创建,这种懒加载的思想其实可以用到我们日常开发中,对于在某些判断分支下不需要创建的数组或对象不要提前进行创建。

Get方法

        接下来看到 ThreadLocal的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();
    }

    
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }


    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

        当调用get时,首先会获取当前执行线程,从线程中获取ThreadLocalMap,再从ThreadLocalMap中获取当前ThreadLocal对应的值。当获取不到ThreadLocalMap,或者当前ThreadLocal没有值时,则进行初始化,若当前线程ThreadLocalMap不存在,则创建当前线程的ThreadLocalMap,并存放入初始值(默认为null),若已存在,则存入当前ThreadLocal和初始值至ThreadLocalMap中。大致流程图如下:

        

         图上和代码上的判定分支流程略有差异,但意思一致,initialValue() 默认return null,所以这里用null表达,可以对照源码理解。

        至此,对ThreadLocal的基本原理我们有了大致上的理解,还有常用remove方法及其他建议自行阅读源码。这里仅对自己的研究做一个总结和分享。

        那么接下来,就可以研究 InheritableThreadLocal 是如何实现可继承性的。

InheritableThreadLocal原理

        InheritableThreadLocal作用是:将在父线程中放入ThreadLocal的值,在子线程中继承使用。正常我们使用ThreadLocal是无法做到这一步的,我们可以写一段程序实验一下。

         这里我们看到输出了null,当然,这是必然的。ThreadLocal设计就是为了保存线程各自的本地变量互不干扰,如果两个线程能访问到同一个,那就变得线程不安全,也违背了它的设计。

        然后我们将ThreadLocal换成InheritableThreadLocal,再试试。

         可以看到获取ThreadLocal中的值成功了。那这样是否会导致线程不安全呢,还能叫线程本地变量么,答案是可以的。接下来看下InheritableThreadLocal的实现,我们就可以理解了。

        首先看Thread类中,可以看到有两个ThreadLocalMap,threadLocals 和inheritableThreadLocals。

        


public
class Thread implements Runnable {
...

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
...
}

        从注释可以看到,简单来说,就是 ThreadLocal 的值放到 threadLocals 里,InheritableThreadLocal 的值放到 inheritableThreadLocals 里。

        那么我们首先可以有一个概念——InheritableThreadLocal 的值会放到 Thread的 inheritableThreadLocals 属性中进行操作。

InheritableThreadLocal源码

        再来看 InheritableThreadLocal 的源码,可以看到它继承了 ThreadLocal,并重写了其中几个方法。它的可继承能力,就在它重写的几个方法中。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    
    protected T childValue(T parentValue) {
        return parentValue;
    }

    
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

大致看下这几个方法:       

  • childValue(): 直接返回传入的parentValue,这个看起来有点不明所以,之后再来展开 
  • getMap(Thread t): 返回了t.inheritableThreadLocals,结合之前的get,set源码的阅读,也就算说如果你是一个InheritableThreadLocal对象,那么它会从线程的inheritableThreadLocals来执行get,set操作
  • createMap(Thread t, T firstValue): 创建一个ThreadLocalMap,并赋值给线程的inheritableThreadLocals

        从上述getMap、createMap方法,结合我们之前看到get,set源码,那就可以理解到“InheritableThreadLocal 的值会放到 Thread的 inheritableThreadLocals 属性中进行操作”这句话的意义。

childValue与parentMap浅拷贝

        那么childValue()是干嘛的呢?看下哪里会调到它,就会发现一片新的天地。顺着childValue点开调用它的地方,可以看到是一个私有构造函数。

    private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

        大致浏览一下,可以看到,它作用是把传入的 parentMap 浅拷贝到当前子线程的ThreadLocalMap中。

        那么再看下这个私有构造函数被谁调用,依次跟下来可以看到这样一个调用链。

        > new Thread(...)

               > init(...)

                       >ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)

                              >ThreadLocalMap(parentMap)

       

init方法

        init方法会在构建Thread的时候执行,在 init 方法中,我们可以看到这么一段:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
...
Thread parent = currentThread();
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}

        当if条件不为 false 时,那么就会触发浅拷贝,将父线程中inheritableThreadLocals中的值复制到子线程的inheritableThreadLocals中。

        这里父线程 parent = currentThread(); 如果不理解的话,想一下,我们是从当前线程再创建线程,那么当前线程其实就是父线程了。

        这里我们再来看两个条件 inheritThreadLocals 和 parent.inheritableThreadLocals != null

        首先看 inheritThreadLocals,可以看到它是由方法入参来的,那么就看下它被调的时候,传了啥。     

        这里看到有两个地方调了它,有acc(AccessControlContext)传false,没有的时候传true,这里 AccessControlContext 是什么可以自行研究,正常new Thread()的时候,默认传true。

        搞定完 inheritThreadLocals,再来看 parent.inheritableThreadLocals != null,这个条件比较好理解。InheritableThreadLocal类重写了 createMap 和 getMap 方法,将所有操作指向了 Thread.inheritableThreadLocals。

        那么只要我们在父线程的 InheritableThreadLocal,调用get或set了,那么 parent.inheritableThreadLocals != null 就成立。

        上述两个条件 inheritThreadLocals && parent.inheritableThreadLocals != null 成立后,那就会触发浅拷贝到子线程的 inheritableThreadLocals 中了,在构建完成后,子线程便可以读取到父线程中ThreadLocal中的值。

        (注:父子线程中的 inheritableThreadLocals 并不共享,依旧保持ThreadLocal特性,仅仅是将父线程 inheritableThreadLocals 的元素浅拷贝到子线程中,子线程有自己的inheritableThreadLocals)

注意项

  1. ThreadLocal与当前线程绑定,如果不用的时候需及时进行remove,否则会导致内存泄露。
  2. InheritableThreadLocal 中是将父线程中的 inheritableThreadLocals 浅拷贝到到子线程中,代码上看就算不使用也会拷贝。(如理解有误请指正)
  3. 父子线程的 inheritableThreadLocals 并不共享,如果你在父线程中执行了remove,子线程中不会受影响,依旧可以get出来。如果有使用到判空get值做判空处理,需注意。
  4. inheritableThreadLocals 只会在初始化时进行拷贝,如果使用线程池需要注意。可看下面例子。
        ThreadLocal<String> t = new InheritableThreadLocal<>();
        t.set("test");

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(() -> {
            System.out.println(t.get());
            t.remove();
        });
        // 确保上面代码执行完毕
        Thread.sleep(1000);

        // 再次执行,在同一线程中,没有新建线程,所以不会进行重新拷贝,输出为null
        executorService.execute(() -> {
            System.out.println(t.get());
        });

        输出为:

 

        

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值