zoukankan      html  css  js  c++  java
  • ThreadLocal原理深入解析


    在上家公司做spark的任务调度系统时,碰到过这么一个需求:
    1.任务由一个线程执行,同时在执行过程中会创建多个线程执行子任务,子线程在执行子任务时又会创建子线程执行子任务的子任务。整个任务结构就像一棵高度为3的树。
    2.每个任务在执行过程中会生成一个任务ID,我需要把这个任务ID传给子线程执行的子任务,子任务同时也会生成自己的任务ID,并把自己的任务ID向自己的子任务传递。
    流程可由下图所示

    解决方案有很多,比如借助外部存储如数据库,或者自己在内存中维护一个存储ID的数据结构。考虑到系统健壮性和可维护性,最后采用了jdk中的InheritableThreadLocal来实现这个需求。
    来看下InheritableThreadLocal的结构

    public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    

    InheritableThreadLocal继承自ThreadLocal,ThreadLocal可以说是一个存储线程私有变量的容器(当然这个说法严格来说不准确,后面我们就知道为什么),而InheritableThreadLocal正如Inheritable所暗示的那样,它是可继承的:使用它可使子线程继承父线程的所有线程私有变量。因此我写了个工具类,底层使用InheritableThreadLocal来存储任务的ID,并且使该ID能够被子线程继承。

    public class InheritableThreadLocalUtils {
    
        private static final ThreadLocal<Integer> local = new InheritableThreadLocal<>();
    
        public static void set(Integer t) {
            local.set(t);
        }
    
        public static Integer get() {
            return local.get();
        }
    
        public static void remove() {
            local.remove();
        }
    }
    
    

    可以通过这个工具类的set方法和get方法分别实现任务ID的存取。然而在Code Review的時候,有同事觉得我这代码写的有问题:原因大概是InheritableThreadLocal在这里只有一个,子线程的任务ID在存储的时候会相互覆盖掉。真的会这样吗?为此我们用代码测试下:

    
    public static void main(String[] args) {
    
        ExecutorService executorService = Executors.newCachedThreadPool();
    
        for(int i=0;i<10;i++){
            executorService.execute(new TaskThread(i));
        }
    
    }
    
    static class TaskThread implements Runnable{
    
        Integer taskId;
    
        public TaskThread(Integer taskId) {
            this.taskId = taskId;
        }
    
        @Override
        public void run() {
            InheritableThreadLocalUtils.set(taskId);
            ExecutorService executor = Executors.newSingleThreadExecutor();
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(InheritableThreadLocalUtils.get());
                }
            });
        }
    }
    
    

    这段代码开启了10个线程标号从0到9,我们在每个线程中将对应的标号存储到InheritableThreadLocal,然后开启一个子线程,在子线程中获取InheritableThreadLocal中的变量。最后的结果如下

    每个线程都准确的获取到了父线程对应的ID,可见并没有覆盖的问题。InheritableThreadLocal确实是用来存储和获取线程私有变量的,但是真实的变量并不是存储在这个InheritableThreadLocal对象中,它只是为我们存取线程私有变量提供了入口而已。因为InheritableThreadLocal只是在ThreadLocal的基础上提供了继承功能,为了弄清这个问题我们研究下ThreadLocal的源码。

    2. ThreadLocal源码解析

    ThreadLocal主要方法有两个,一个set用来存储线程私有变量,一个get用来获取线程私有变量。

    2.1 set方法源码解析

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    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 t = Thread.currentThread()获取了当前线程实例t,继续跟进第二行的getMap方法,

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    

    t是线程实例,而threadLocals明显是t的一个成员变量,进入一探究竟

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

    ThreadLocalMap是个什么结构?

    static class ThreadLocalMap {
    
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
    
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    
    

    ThreadLocalMap是类Thread中的一个静态内部类,看起来像一个HashMap,但和HashMap又有些不一样(关于它们的区别后面会讲),那我们就把它当一个特殊的HashMap好了。因此set方法中第二行代码
    ThreadLocalMap map = getMap(t)是通过线程实例t得到一个ThreadLocalMap。接下来的代码

    if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    
    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    

    如果这个threadlocalmap为null,先创建一个threadlocalmap,然后以当前threadlocal对象为key,以要存储的变量为值存储到threadlocalmap中。

    2.2 get方法源码解析

    
    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }
    

    首先获取当前线程实例t,然后通过getMap(t)方法得到threadlocalmap(ThreadLocalMap是Thread的成员变量)。若这个map不为null,则以threadlocal为key获取线程私有变量,否则执行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;
    }
    
    protected T initialValue() {
        return null;
    }
    

    首先获取threadlocal的初始化值,默认为null,可以通过重写自定义该值;如果threadlocalmap为null,先创建一个;以当前threadlocal对象为key,以初始化值为value存入map中,最后返回这个初始化值。

    2.3 ThreadLocal源码总结

    总的来说,ThreadLocal的源码并不复杂,但是逻辑很绕。现总结如下:

    • 1.ThreadLocal对象为每个线程存取私有的本地变量提供了入口,变量实际存储在线程实例的内部一个叫ThreadLocalMap的数据结构中。
    • 2.ThreadLocalMap是一个类HashMap的数据结构,Key为ThreadLoca对象(其实是一个弱引用),Value为要存储的变量值。
    • 3.使用ThreadLocal进行存取,其实就是以ThreadLocal对象为隐含的key对各个线程私有的Map进行存取。

    可以用下图的内存图像帮助理解和记忆

    3. ThreadLocalMap详解

    先看源码

    static class ThreadLocalMap {
    
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
    
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    

    3.1 ThreadLocalMap的key为弱引用

    ThreadLocalMap的key并不是ThreadLocal,而是WeakReference<ThreadLocal>,这是一个弱引用,说它弱是因为如果一个对象只被弱引用引用到,那么下次垃圾收集时就会被回收掉。如果引用ThreadLocal对象的只有ThreadLocalMap的key,那么下次垃圾收集过后该key就会变为null。

    3.2 为何要用弱引用

    减少了内存泄漏。试想我曾今存储了一个ThreadLocal对象到ThreadLocalMap中,但后来我不需要这个对象了,只有ThreadLocalMap中的key还引用了该对象。如果这是个强引用的话,该对象将一直无法回收。因为我已经失去了其他所有该对象的外部引用,这个ThreadLocal对象将一直存在,而我却无法访问也无法回收它,导致内存泄漏。又因为ThreadLocalMap的生命周期和线程实例的生命周期一致,只要该线程一直不退出,比如线程池中的线程,那么这种内存泄漏问题将会不断积累,直到导致系统奔溃。而如果是弱引用的话,当ThreadLocal失去了所有外部强引用的话,下次垃圾收集该ThreadLocal对象将被回收,对应的ThreadLocalMap中的key将为null。下次get和set方法被执行时将会对key为null的Entry进行清理。有效的减少了内存泄漏的可能和影响。

    3.3 如何真正避免内存泄漏

    • 及时调用ThreadLocal的remove方法
    • 及时销毁线程实例

    4. 总结

    ThreadLocal为我们存取线程私有变量提供了入口,变量实际存储在线程实例的map结构中;使用它可以让每个线程拥有一份共享变量的拷贝,以非同步的方式解决多线程对资源的争用

  • 相关阅读:
    maven打包将依赖和配置外置的配置方式
    AngularJS零碎总结
    Python零碎总结
    win10 平台 elasticsearch 与 elasticsearch-head 的安装
    DDD中的分层架构
    读书有感--------软件的设计原则
    DDD初探
    HTTP could not register URL http://+:86/. 设置VS默认以管理员权限打开
    .net 实战 根据configuration选项生成不同的config文件
    asp.net mvc5轻松实现插件式开发
  • 原文地址:https://www.cnblogs.com/takumicx/p/9320881.html
Copyright © 2011-2022 走看看