源码分析:InheriableThreadLocal传递数据的原理和ThreadLocal导致的内存泄露原因

Posted talk.push

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码分析:InheriableThreadLocal传递数据的原理和ThreadLocal导致的内存泄露原因相关的知识,希望对你有一定的参考价值。

文章目录

ThreadLocal有什么问题?

挺巧的,去年今天写了这个demo,今天复盘的时候又想起了ThreadLocal。ThreadLocal可以为每一个线程保存一份数据,通常可以用来解决多线程环境下变量的竞争。但却无法做到父子线程这种上下文环境下变量的父子线程传递,也就是说仅仅靠ThreadLocal是无法做到在子线程中获取主线程中set的变量的。那么,InheritableThreadLocal可继承ThreadLocal就是来解决这个问题的。

package com.jeff.study.concurrent.lock;

/**
 * @author jeffSmile
 * @date 2020-04-28 上午 11:30
 * @desc
 */
public class TestThreadLocal 

    //    public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) 
        //主线程必须在子线程创建之前set共享变量,否则子线程还是访问不到
        threadLocal.set("hello world");
        Thread thread = new Thread(new Runnable() 
            @Override
            public void run() 
                System.out.println("thread:" + threadLocal.get());
            
        );
//        threadLocal.set("hello world");
        thread.start();
        System.out.println("main:" + threadLocal.get());
    


InheriableThreadLocal如何复制数据到子线程?

搞清楚了问题,我们接下来就看看InheriableThreadLocal是如何把主线程中set进去的数据,复制到子线程中的。这样从Thread的构造器说起,这里搬出源码,只保留重要的部分。

public Thread(Runnable target) 
        init(null, target, "Thread-" + nextThreadNum(), 0);


 private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) 
        init(g, target, name, stackSize, null, true);


private void init(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();
      .......
      ......
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    

判断主线程parent的inheritableThreadLocals变量是否为空,这是一个Map变量。如果不为空则复制这个Map给子线程的相应变量。

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

这个变量的赋值是由主线程在执行InheritableThreadLocal.set(data);时设置进去的。第一次set时当前Thread(主线程)的threadLocals变量为空,所以执行createMap方法。

   public void set(T value) 
        Thread t = Thread.currentThread();
        //第一次时thread的threadLocals这个map为空
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    

由于我们使用的InheriableThreadLocal,所以执行的createMap也是InheriableThreadLocal的所实现方法。InheriableThreadLocal中直接创建了一个ThreadLocalMap赋值给了成员变量inheritableThreadLocals。

   /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) 
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    

所以,当我们看到Thread创建时的如下代码时,知道子线程会复制一份主线程的this.inheritableThreadLocals变量。

 if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

子线程复制父线程变量细节剖析

接下来就分析下复制的原理:

this.inheritableThreadLocals =  ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

createInheritedMap是ThreadLocal中的静态方法,这个方法很简单直接创建一个ThreadLocalMap返回了,参数就是父Thread的内部变量inheritableThreadLocals。

  static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) 
        return new ThreadLocalMap(parentMap);
    

那么,数据传递的机制一定在ThreadLcoalMap的构造器里了。这里就是把parent线程ThreadLocalMap中的Entry[]数组拿出来遍历,组装成新的Entry再放进child线程的ThreadLocalMap的Entry[]数组。

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

这样就实现了数据的复制。这样以来在子线程中使用threadlcoal.get()方法就可以从child线程自己的ThreadLocalMap中拿到数据了。

ThreadLocalMap是个什么鬼?

//ThreadLocal.java
 /**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    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;
            
	             /**
	         * The initial capacity -- MUST be a power of two.
	         */
	        private static final int INITIAL_CAPACITY = 16;
	
	        /**
	         * The table, resized as necessary.
	         * table.length MUST always be a power of two.
	         */
	        private Entry[] table;
	
	        /**
	         * The number of entries in the table.
	         */
	        private int size = 0;
	
	        /**
	         * The next size value at which to resize.
	         */
	        private int threshold; // Default to 0
        
     

首先,ThreadLocalMap是ThreadLocal的一个静态内部类。看下这个静态内部类头顶的注释:

/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*
* ThreadLocalMap是自定义的哈希映射,仅适用于维护线程局部值。
* 没有操作导出到ThreadLocal类之外。
* 该类是包私有的,以允许在Thread类中的声明字段。
* 为了帮助处理非常长的使用寿命,哈希表条目使用WeakReferences作为键。
* 但是,由于不使用参考队列,因此仅在表空间不足时,才保证删除过时的条目。
*/

在ThreadLocalMap中定义了Entry静态内部类,这个类继承了WeakReference<ThreadLocal<?>>。看下关于这个Entry的注释,也就是说Entry中利用父类WeakReference的referent成员变量来保存key(threadLcoal)。

//ThreadLocalMap.java
      /**
         * 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.
         * 
         * 此哈希映射中的条目使用其主引用字段作为键(始终是ThreadLocal对象)
         * 扩展了WeakReference。 
         * 注意,空键(即entry.get()== null)意味着不再引用该键,
         * 因此可以从表中删除该条目。 在下面的代码中,此类条目称为“陈旧条目”
         */
        static class Entry extends WeakReference<ThreadLocal<?>> 
            /** The value associated with this ThreadLocal. */
            Object value;

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

说白了,threadLcoal在ThreadLocalMap中就是一个WeakReference内部维护的弱引用。弱点引用变量只能生存到下一次GC之前,无论内存是否充足都会被回收掉。
只所以设计成弱引用,是为了应对线程长期存活导致threadLocal因为强引用导致无法回收的情况,比如在线程池中线程是长期存活的,如果这个线程中长期对ThreadLocal强引用,那么将可能导致内存泄露。
但是ThreadLocal虽然设计为弱引用,但还是可能导致内存泄露!
这是因为尽管ThreadLocal作为弱引用被下一次GC回收而变为null,但是value却是强引用的,那么在ThreadLocalMap中对应的Entry是无效的,但却不会释放内存!除非你主动调用ThreadLocal#remove方法.在这个方法中会对key==null的Entry进行回收。

  /**
         * Remove the entry for key.
         */
        private void remove(ThreadLocal<?> key) 
            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) 
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                
            
        


 /**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        private int expungeStaleEntry(int staleSlot) 
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) 
                ThreadLocal<?> k = e.get();
                //清除无效Entry
                if (k == null) 
                    e.value = null;
                    tab[i] = null;
                    size--;
                 else 
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) 
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    
                
            
            return i;
        

以上是关于源码分析:InheriableThreadLocal传递数据的原理和ThreadLocal导致的内存泄露原因的主要内容,如果未能解决你的问题,请参考以下文章

Mybatis源码分析

Spring源码分析专题——目录

ARouter源码分析

Handler源码分析

Eureka源码分析(六) TimedSupervisorTask

[Netty源码分析]ByteBuf(一)