美文网首页多线程
ThreadLocal的使用和内存泄漏

ThreadLocal的使用和内存泄漏

作者: 念䋛 | 来源:发表于2021-10-10 16:53 被阅读0次

    关于使用方面的就借用这边大神的文章
    转载 https://www.jianshu.com/p/3c5d7f09dfbd
    文章的最后也会分析到ThreadLocal如何会内存溢出
    ThreadLocal
    threadlocal使用方法很简单

    static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
    sThreadLocal.set()
    sThreadLocal.get()
    

    threadlocal而是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据,官方解释如下。

    /**
     * This class provides thread-local variables.  These variables differ from
     * their normal counterparts in that each thread that accesses one (via its
     * {@code get} or {@code set} method) has its own, independently initialized
     * copy of the variable.  {@code ThreadLocal} instances are typically private
     * static fields in classes that wish to associate state with a thread (e.g.,
     * a user ID or Transaction ID).
     */
    

    大致意思就是ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。

    做个不恰当的比喻,从表面上看ThreadLocal相当于维护了一个map,key就是当前的线程,value就是需要存储的对象。

    这里的这个比喻是不恰当的,实际上是ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。。

    作为一个存储数据的类,关键点就在get和set方法。
    //set 方法
    public void set(T value) {
          //获取当前线程
          Thread t = Thread.currentThread();
          //实际存储的数据结构类型
          ThreadLocalMap map = getMap(t);
          //如果存在map就直接set,没有则创建map并set
          if (map != null)
              map.set(this, value);
          else
              createMap(t, value);
      }
    
    //getMap方法
    ThreadLocalMap getMap(Thread t) {
          //thred中维护了一个ThreadLocalMap
          return t.threadLocals;
     }
     
    //createMap
    void createMap(Thread t, T firstValue) {
          //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
          t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    

    从上面代码可以看出每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。

    Thread

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

    Thread中关于ThreadLocalMap部分的相关声明,接下来看一下createMap方法中的实例化过程。

    ThreadLocalMap
    set方法

    //Entry为ThreadLocalMap静态内部类,对ThreadLocal的若引用
    //同时让ThreadLocal和储值形成key-value的关系
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
    
        Entry(ThreadLocal<?> k, Object v) {
               super(k);
                value = v;
        }
    }
    
    //ThreadLocalMap构造方法
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //内部成员数组,INITIAL_CAPACITY值为16的常量
            table = new Entry[INITIAL_CAPACITY];
            //位运算,结果与取模相同,计算出需要存放的位置
            //threadLocalHashCode比较有趣
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
    }
    

    通过上面的代码不难看出在实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。

    前面讲过每个线程Thread持有一个ThreadLocalMap类型的实例threadLocals,结合此处的构造方法可以理解成每个线程Thread都持有一个Entry型的数组table,而一切的读取过程都是通过操作这个数组table完成的。

    显然table是set和get的焦点,在看具体的set和get方法前,先看下面这段代码。

    //在某一线程声明了ABC三种类型的ThreadLocal
    ThreadLocal<A> sThreadLocalA = new ThreadLocal<A>();
    ThreadLocal<B> sThreadLocalB = new ThreadLocal<B>();
    ThreadLocal<C> sThreadLocalC = new ThreadLocal<C>();
    

    由前面我们知道对于一个Thread来说只有持有一个ThreadLocalMap,所以ABC对应同一个ThreadLocalMap对象。为了管理ABC,于是将他们存储在一个数组的不同位置,而这个数组就是上面提到的Entry型的数组table。

    那么问题来了,ABC在table中的位置是如何确定的?为了能正常够正常的访问对应的值,肯定存在一种方法计算出确定的索引值i,show me code。

      //ThreadLocalMap中set方法。
      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);
    
                //遍历tab如果已经存在则更新值
                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;
                //满足条件数组扩容x2
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    

    在ThreadLocalMap中的set方法与构造方法能看到以下代码片段。

    int i = key.threadLocalHashCode & (len-1)
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
    简而言之就是将threadLocalHashCode进行一个位运算(取模)得到索引i,threadLocalHashCode代码如下。

        //ThreadLocal中threadLocalHashCode相关代码.
        
        private final int threadLocalHashCode = nextHashCode();
    
        /**
         * The next hash code to be given out. Updated atomically. Starts at
         * zero.
         */
        private static AtomicInteger nextHashCode =
            new AtomicInteger();
    
        /**
         * The difference between successively generated hash codes - turns
         * implicit sequential thread-local IDs into near-optimally spread
         * multiplicative hash values for power-of-two-sized tables.
         */
        private static final int HASH_INCREMENT = 0x61c88647;
    
        /**
         * Returns the next hash code.
         */
        private static int nextHashCode() {
            //自增
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
    

    因为static的原因,在每次new ThreadLocal时因为threadLocalHashCode的初始化,会使threadLocalHashCode值自增一次,增量为0x61c88647。

    0x61c88647是斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突,已初始容量16为例,hash并与15位运算计算数组下标结果如下:

    hashCode 数组下标
    0x61c88647 7
    0xc3910c8e 14
    0x255992d5 5
    0x8722191c 12
    0xe8ea9f63 3
    0x4ab325aa 10
    0xac7babf1 1
    0xe443238 8
    0x700cb87f 15
    总结如下:

    对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。
    对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。
    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();
    }
        
    //ThreadLocalMap中getEntry方法
    private Entry getEntry(ThreadLocal<?> key) {
           int i = key.threadLocalHashCode & (table.length - 1);
           Entry e = table[i];
           if (e != null && e.get() == key)
                return e;
           else
                return getEntryAfterMiss(key, i, e);
       }
    

    理解了set方法,get方法也就清楚明了,无非是通过计算出索引直接从数组对应位置读取即可。

    ThreadLocal实现主要涉及Thread,ThreadLocal,ThreadLocalMap这三个类。关于ThreadLocal的实现流程正如上面写的那样,实际代码还有许多细节处理的部分并没有在这里写出来。

    ThreadLocal特性
    ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是

    Synchronized是通过线程等待,牺牲时间来解决访问冲突
    ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
    正因为ThreadLocal的线程隔离特性,使他的应用场景相对来说更为特殊一些。在android中Looper、ActivityThread以及AMS中都用到了ThreadLocal。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。

    关于内存泄漏的分析

    我们都知道ThreadLocal有一个静态内部类ThreadLocalMap,而这个内部类里的成员变量private Entry[] table;
    table中每一个Entry就存放了value,其中key就是ThreadLocal对象本身,文章中提到了一个线程如果声明了多个ThreadLocal那么就会有多个Entry,存放进table中,而ThreadLocalMap并没有在ThreadLocal使用,而是在Thread里面使用,可以看到Thread中的成员变量ThreadLocal.ThreadLocalMap threadLocals = null;
    我们又可以看到Entry继承了WeakReference<ThreadLocal<?>> 那Entry 中的key就是弱引用

            static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    

    那我们分析一下
    1.Thread当前线程引用ThreadLocalMap,ThreadLocalMap中的Entry数组table,其中Entry中的key为弱引用,是当前方法的ThreadLocal对象,value是自己存放的值
    2.执行方法的时候,在栈中调用方法,并声明了ThreadLocal对象,也就上面说的Entry中的key,那么此时对于ThreadLocal来讲,有两处被引用其中栈中的方法申明的ThreadLocal对象是强引用,而Entry中的key是ThreadLocal对象是弱引用
    3.当方法执行结束后ThreadLocal对象强引用没有了,只有Entry中的key的弱引用,这个弱引用会被GC回收掉
    4.如果线程结束后Thread被回收到线程池中,那么就会出现这么一种情况Thread的成员变量ThreadLocalMap中的table中Entry的key为null,但是value依然存在,而且ThreadLocalMap的table是一直被Thread强引用着,不会被GC回收,就会一直留着,如果线程多次被使用,table的Entry就会变多,而且都是key为null,只保留了value,这样这种没用Entry越来越多,就造成了内存泄漏.
    为了避免这种情况,使用结束后remove就可以了.

    相关文章

      网友评论

        本文标题:ThreadLocal的使用和内存泄漏

        本文链接:https://www.haomeiwen.com/subject/iqiholtx.html