ThreadLocal和InheritableThreadLocal详解,基本原理及注意项 父子线程数据共享

发布时间 2023-12-22 18:43:55作者: 山河永慕~

一、ThreadLocal介绍

在多线程环境下访问同一个线程的时候会出现并发问题,特别是多个线程同时对一个变量进行写入操作时,为了保证线程的安全,通常会进行加锁来保证线程的安全,但是加锁又会造成效率的降低;ThreadLocal是jdk提供的除了加锁之外保证线程安全的方法,其实现原理是在Thread类中定义了两个ThreadLocalMap类型变量threadLocals、inheritableThreadLocals用来存储当前操作的ThreadLocal的引用及变量对象,这样就可以把当前线程的变量和其他的线程的变量之间进行隔离,从而实现了线程的安全性。

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

二、ThreadLocal的简单示例

首先定义一个ThreadLocal全局变量,在main方法中开启两个线程,在线程启用之前先设置变量然后再线程内部和外部获取变量,发现输出结果有什么不同吗?疑问我们后面解答。

public class Test1 {
    public static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, InterruptedException {
        threadLocal.set("主线程1。。。");
        System.out.println("线程1:" + threadLocal.get());
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程1:" + Thread.currentThread().getName() + ":" + threadLocal.get());
            }
        }).start();

        Thread.sleep(1000);
        threadLocal.set("主线程2。。。");
        System.out.println("线程2:" + threadLocal.get());
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程2:" + Thread.currentThread().getName() + ":" + threadLocal.get());
            }
        }).start();
        //删除本地内存中的变量
        threadLocal.remove();
    }

}

输出结果如下:

线程1:主线程1。。。
子线程1:Thread-0:null
线程2:主线程2。。。
子线程2:Thread-1:null

三、ThreadLocal原理及源码分析
Thread类有两个变量threadLocals和inheritableThreadLocals,这两个变量都是ThreadLocal类的内部类ThreadLocalMap类型变量,ThreadLocalMap是一个类似于HashMap类型的容器类,默认情况下两个变量都为null,只有当第一次调用ThreadLocal的get或set方法的时候才会创建它们。

    /* 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类中,都是存放在当前线程的变量之中,也就是说存放在当前线程的上下文空间中,其线程本身相当于变量的一个承载工具,通过set方法将变量添加到线程的threadLocals变量中,通过get方法能够从它的threadLocals变量中获取变量。如果线程一直不终止,那么这个本地变量将会一直存放在它的threadLocals变量中,所以可以通过remove方法删除本地变量。
set方法

    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
          //以当前线程为参数,查找线程的变量threadLocals(1)
        ThreadLocalMap map = getMap(t);
        //如果线程变量map不为null,直接添加本地变量,
          //key为当前定义的ThreadLocal变量的this引用,值为添加到本地的变量值
        if (map != null) {
            map.set(this, value);
        } else {
           //如果为null,这创建一个ThreadLocalMap对象赋值给当前线程t(2)
            createMap(t, value);
        }
    }

上述代码(1)处获取当前线程的threadLocals变量,方法如下:

//获取线程的threadLocals变量,并将本地变量和ThreadLocal对象引用绑定到变量上    
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

上述代码(2)处创建ThreadLocalMap对象并初始化当前线程的threadLocals变量,方法如下:

    void createMap(Thread t, T firstValue) {
       //this是当前ThreadLocal对象的引用,firstValue是变量值
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  • get方法

    public T get() {
       //获取当前线程|调用者的线程(1)
        Thread t = Thread.currentThread();
       //获取当前线程的threadLocals变量(2)
        ThreadLocalMap map = getMap(t);
       //如果ThreadLocalMap变量不为null,就可以在map中查找到本地变量的值(3)
        if (map != null) {
            //通过当前ThreadLocal对象的this引用获取变量中
            ThreadLocalMap.Entry e = map.getEntry(this);
          // 如果变量值不为空,则转换为指定的类型返回
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
      //threadLocals变量为null,则对当前线程的threadLocals的变量进行初始化
        return setInitialValue();
    }

    private T setInitialValue() {
        //调用抽象初始化方法,默认:null
        T value = initialValue();
      //获取当前线程
        Thread t = Thread.currentThread();
       //查找当前线程的threadLocals变量
        ThreadLocalMap map = getMap(t);
        //如果map不为null,则直接添加本地变量,key为当前ThreadLocal的this引用,value为本地变量
        if (map != null) {
            map.set(this, value);
        } else {
            //首次添加,创建对应的threadLocals变量
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }
  • remove方法的实现
     public void remove() {
        //获取当前线程的threadLocals变量,如果变量非null,则删除当前ThreadLocal引用对应的变量值
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             //删除ThreadLocalMap中存储的数据(1)
             m.remove(this);
         }
     }

上述(1)中的remove方法是ThreadLocal.ThreadLocalMap类的方法:

        private void remove(ThreadLocal<?> key) {
            //当前ThreadLocal变量所在的table数组位置
            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)]) {
                if (e.get() == key) {
                  //调用WeakReference的clear方法清除对ThreadLocal的弱引用
                    e.clear();
                  //清理key为null的元素
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

四、ThreadLocal不支持继承性
​ 在同一个ThreadLocal变量在父线程中被设置值之后,在子线程中是获取不到的(由二中的实例输出可以看出),threadLocals中为当前调用线程的本地变量,所以子线程是无法获取父线程的变量的;一开始我们介绍的时候说Thread类中还有一个inheritableThreadLocals变量,其值是存储的子线程的变量,所以可以通过InheritableThreadLocal类获取父线程的变量;

五、InheritableThreadLocal类源码简介
InheritableThreadLocal类是ThreadLocal类的子类,重写了chidValue、getMap、createMap三个方法,其中createMap方法在被调用的时候创建的是inheritableThreadLocals变量值(ThreadLocal类中创建的是threadLocals变量的值),getMap方法在被get或set方法调用的时候返回的也是线程的inheritableThreadLocals变量。

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

那chidValue方法又是在哪里被调用呢?它会在ThreadLocalMap的中被调用:

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

            for (Entry e : parentTable) {
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                       //调用重写的childValue方法
                        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++;
                    }
                }
            }
        }

子线程是如何继承父线程的变量的,看Thread类的如下代码:

    private Thread(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
                //获取当前线程(父线程)
        Thread parent = currentThread();
        //安全校验
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security manager doesn't have a strong opinion
               on the matter, use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(
                        SecurityConstants.SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;//设置为当前线程组
        this.daemon = parent.isDaemon();//判定是否是守护线程同父线程
        this.priority = parent.getPriority();//优先级同父线程
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        //如果是否初始化线程的inheritThreadLocals变量为true并且父线程的inheritThreadLocals变量不为null
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
           //设置子线程的inheritThreadLocals变量为父线程的inheritThreadLocals变量
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        this.tid = nextThreadID();
    }

InheritableThreadLocal类通过重写方法在调用get、set方法的时候会调用重写的方法,调用方法时会对线程的

inheritableThreadLocals变量进行初始化,在对子线程进行初始化的时候会将子线程的

inheritableThreadLocals变量赋值为父线程的inheritableThreadLocals变量值,这样就实现了子线程继承父线程问题。

六、拓展ThreadLocal使用不当引起的内存泄漏问题
首先了解下什么是弱引用,弱引用也就是GC会主动的帮你把内存回收掉,但是当弱引用指定的对象有强引用指向时GC则不会回收对象。

ThreadLocalMap的内部实现实际上是一个Entry集合,Entry类继承了WeakReference类,表示其是一个弱引用对象,其构造函数super调用了父类的构造函数,传递了k变量,即ThreadLocal对象的引用,也就是说Map集合中存储的是ThreadLocal的弱引用。

    static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        }

如果当前线程一直存在且没有调用ThreadLocal的remove方法,那么ThreadLocal会在下次GC的时候将弱引用对象回收,这样就会造成ThreadLocalMap对象中的Entry对象的key为null,但是value值还是存在强引用关系,这样就会造成内存泄漏问题;引用链关系如下:

Thread->ThreadLocalMap->Entry->value

弱引用会在没有强引用的情况下在下次GC的时候自动的回收掉,每次使用完后要记得主动的调用remove方法,而且ThreadLocal每次调用get、set、remove的时候都会直接或者间接的调用expungeStaleEntry方法清除掉key为null的Entry,从而避免了内存泄漏。
七、拓展InheritableThreadLocal类子线程使用线程池更改存储变量值不变问题

 

public class Test {
    public static final ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    private static ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, InterruptedException {
        threadLocal.set("主线程1。。。");
        System.out.println("线程1:" + threadLocal.get());
        executorService.submit(TtlRunnable.get(() -> System.out.println("子线程:" + Thread.currentThread().getName() + ":" + threadLocal.get())));
        Thread.sleep(1000);
        threadLocal.set("主线程2。。。");
        System.out.println("线程2:" + threadLocal.get());
        executorService.submit(TtlRunnable.get(() -> System.out.println("子线程:" + Thread.currentThread().getName() + ":" + threadLocal.get())));
    }

}

 

输出结果:

线程1:主线程1。。。
子线程:pool-1-thread-1:主线程1。。。
线程2:主线程2。。。
子线程:pool-1-thread-1:主线程1。。。

可以看到在运行线程2之前会修改InheritableThreadLocal内部存的值,但是在线程池内部获取值还是原来的,这其实又涉及到了线程池等会池化复用线程情况下,提供ThreadLocal值传递问题,推荐使用阿里开源的TTLhttps://github.com/alibaba/transmittable-thread-local

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

注意 使用静态和不使用静态时候

  使用静态的 InheritableThreadLocal 线程池复用时候不会有问题

  ThreadLocal是线程本地变量,每个线程有自己的副本;而InheritableThreadLocal具有继承性,在创建子线程时,子线程可以继承父线程的变量副本。

private static final InheritableThreadLocal<T> t1 = new InheritableThreadLocal<T>();
        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());
        });