• ThreadLocal源码分析


     1. ThreadLocal的内部结构

    1.1 常见的误解

    通常,如果我们不去看源代码的话,我猜ThreadLocal是这样子设计的:每个ThreadLocal类都创建一个Map,然后用线程的ID threadID作为Mapkey,要存储的局部变量作为Mapvalue,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal就是这样设计的。

    1.2 核心结构

    但是,JDK后面优化了设计方案,现时JDK8 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的keyThreadLocal实例本身,value才是真正要存储的值Object

    (1) 每个Thread线程内部都有一个Map (ThreadLocalMap) (2) Map里面存储ThreadLocal对象(key)和线程的变量副本(value) (3)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。 (4)对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

     

    1.3 这样设计的好处

    这个设计与我们一开始说的设计刚好相反,这样设计有如下两个优势:

    (1) 这样设计之后每个Map存储的Entry数量就会变少,因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。

    (2) 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。

     

    2. ThreadLocal的核心方法源码

    基于ThreadLocal的内部结构,我们继续探究一下ThreadLocal的核心方法源码,更深入的了解其操作原理。

    除了构造之外, ThreadLocal对外暴露的方法有以下4个:

    方法声明描述
    protected T initialValue() 返回当前线程局部变量的初始值
    public void set( T value) 设置当前线程绑定的局部变量
    public T get() 获取当前线程绑定的局部变量
    public void remove() 移除当前线程绑定的局部变量

    其实get,set和remove逻辑是比较相似的,我们要研究清楚其中一个,其他也就明白了。

    2.1 get方法

    (1 ) 源码和对应的中文注释

      /**
         * 返回当前线程中保存ThreadLocal的值
         * 如果当前线程没有此ThreadLocal变量,
         * 则它会通过调用{@link #initialValue} 方法进行初始化值
         *
         * @return 返回当前线程对应此ThreadLocal的值
         */
        public T get() {
            // 获取当前线程对象
            Thread t = Thread.currentThread();
            // 获取此线程对象中维护的ThreadLocalMap对象
            ThreadLocalMap map = getMap(t);
            // 如果此map存在
            if (map != null) {
                // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
                ThreadLocalMap.Entry e = map.getEntry(this);
                // 找到对应的存储实体 e 
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    // 获取存储实体 e 对应的 value值
                    // 即为我们想要的当前线程对应此ThreadLocal的值
                    T result = (T)e.value;
                    return result;
                }
            }
            // 如果map不存在,则证明此线程没有维护的ThreadLocalMap对象
            // 调用setInitialValue进行初始化
            return setInitialValue();
        }
    
        /**
         * set的变样实现,用于初始化值initialValue,
         * 用于代替防止用户重写set()方法
         *
         * @return the initial value 初始化后的值
         */
        private T setInitialValue() {
            // 调用initialValue获取初始化的值
            T value = initialValue();
            // 获取当前线程对象
            Thread t = Thread.currentThread();
            // 获取此线程对象中维护的ThreadLocalMap对象
            ThreadLocalMap map = getMap(t);
            // 如果此map存在
            if (map != null)
                // 存在则调用map.set设置此实体entry
                map.set(this, value);
            else
                // 1)当前线程Thread 不存在ThreadLocalMap对象
                // 2)则调用createMap进行ThreadLocalMap对象的初始化
                // 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
                createMap(t, value);
            // 返回设置的值value
            return value;
        }
    
        /**
         * 获取当前线程Thread对应维护的ThreadLocalMap 
         * 
         * @param  t the current thread 当前线程
         * @return the map 对应维护的ThreadLocalMap 
         */
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
        /**
         *创建当前线程Thread对应维护的ThreadLocalMap 
         *
         * @param t 当前线程
         * @param firstValue 存放到map中第一个entry的值
         */
        void createMap(Thread t, T firstValue) {
            //这里的this是调用此方法的threadLocal
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }

    2 ) 代码执行流程

    A. 首先获取当前线程

    B. 根据当前线程获取一个Map

    C. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到E

    D. 如果e不为null,则返回e.value,否则转到E

    E. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

    总结: 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。

    2.2 set方法

    (1 ) 源码和对应的中文注释

    /**
         * 设置当前线程对应的ThreadLocal的值
         *
         * @param value 将要保存在当前线程对应的ThreadLocal的值
         */
        public void set(T value) {
            // 获取当前线程对象
            Thread t = Thread.currentThread();
            // 获取此线程对象中维护的ThreadLocalMap对象
            ThreadLocalMap map = getMap(t);
            // 如果此map存在
            if (map != null)
                // 存在则调用map.set设置此实体entry
                map.set(this, value);
            else
                // 1)当前线程Thread 不存在ThreadLocalMap对象
                // 2)则调用createMap进行ThreadLocalMap对象的初始化
                // 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
                createMap(t, value);
        }

    (2 ) 代码执行流程

    A. 首先获取当前线程,并根据当前线程获取一个Map

    B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)

    C. 如果Map为空,则给该线程创建 Map,并设置初始值

    2.3 remove方法

    (1 ) 源码和对应的中文注释

    /**
         * 删除当前线程中保存的ThreadLocal对应的实体entry
         */
         public void remove() {
            // 获取当前线程对象中维护的ThreadLocalMap对象
             ThreadLocalMap m = getMap(Thread.currentThread());
            // 如果此map存在
             if (m != null)
                // 存在则调用map.remove
                // 以当前ThreadLocal为key删除对应的实体entry
                 m.remove(this);
         }

    2 ) 代码执行流程

    A. 首先获取当前线程,并根据当前线程获取一个Map

    B. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry

    2.4 initialValue方法

    /**
      * 返回当前线程对应的ThreadLocal的初始值
      
      * 此方法的第一次调用发生在,当线程通过{@link #get}方法访问此线程的ThreadLocal值时
      * 除非线程先调用了 {@link #set}方法,在这种情况下,
      * {@code initialValue} 才不会被这个线程调用。
      * 通常情况下,每个线程最多调用一次这个方法。
      *
      * <p>这个方法仅仅简单的返回null {@code null};
      * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
      * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
      * 通常, 可以通过匿名内部类的方式实现
      *
      * @return 当前ThreadLocal的初始值
      */
    protected T initialValue() {
        return null;
    }

    此方法的作用是 返回该线程局部变量的初始值。

    (1) 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。

    (2)这个方法缺省实现直接返回一个null

    (3)如果想要一个除null之外的初始值,可以重写此方法。(备注: 该方法是一个protected的方法,显然是为了让子类覆盖而设计的)

    3. ThreadLocalMap源码分析

    3.1 基本结构

    ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。

     

     (1) 成员变量

     /**
         * 初始容量 —— 必须是2的整次幂
         */
        private static final int INITIAL_CAPACITY = 16;
    
        /**
         * 存放数据的table,Entry类的定义在下面分析
         * 同样,数组长度必须是2的冥。
         */
        private Entry[] table;
    
        /**
         * 数组里面entrys的个数,可以用于判断table当前使用量是否超过负因子。
         */
        private int size = 0;
    
        /**
         * 进行扩容的阈值,表使用量大于它的时候进行扩容。
         */
        private int threshold; // Default to 0
        
        /**
         * 阈值设置为长度的2/3
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

    (2) 存储结构 - Entry

    // 在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了
    // 另外,Entry继承WeakReference,使用弱引用,可以将ThreadLocal对象的生命周期和线程生命周期解绑,持有对ThreadLocal的弱引用,可以使得ThreadLocal在没有其他强引用的时候被回收掉,这样可以避免因为线程得不到销毁导致ThreadLocal对象无法被回收
    
    static class Entry extends WeakReference<ThreadLocal> {
        /** The value associated with this ThreadLocal. */
        Object value;
    
        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
    }

    3.2 hash冲突的解决

    ThreadLocal使用的是自定义的ThreadLocalMap,接下来我们来探究一下ThreadLocalMap的hash冲突解决方式。

    (1) 先回顾ThreadLocal的set() 方法

     public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocal.ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
        
        ThreadLocal.ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
        }
    • 代码很简单,获取当前线程,并获取当前线程的ThreadLocalMap实例(从getMap(Thread t)中很容易看出来)。

    • 如果获取到的map实例不为空,调用map.set()方法,否则调用构造函数 ThreadLocal.ThreadLocalMap(this, firstValue)实例化map。

    可以看出来线程中的ThreadLocalMap使用的是延迟初始化,在第一次调用get()或者set()方法的时候才会进行初始化。

    (2) 下面来看看构造函数ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //初始化table
            table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
            //计算索引
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //设置值
            table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
            size = 1;
            //设置阈值
            setThreshold(INITIAL_CAPACITY);
        }

    主要说一下计算索引,firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

    • 关于& (INITIAL_CAPACITY - 1),这是取模的一种方式,对于2的幂作为模数取模,用此代替%(2^n),这也就是为啥容量必须为2的冥,在这个地方也得到了解答。

    • 关于firstKey.threadLocalHashCode

    private final int threadLocalHashCode = nextHashCode();
        
        private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
        private static AtomicInteger nextHashCode =  new AtomicInteger();
                
        private static final int HASH_INCREMENT = 0x61c88647;

    这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,这个值和斐波那契散列有关(这是一种乘数散列法,只不过这个乘数比较特殊,是32位整型上限2^32-1乘以黄金分割比例0.618....的值2654435769,用有符号整型表示就是-1640531527,去掉符号后16进制表示为0x61c88647),其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突。

    (3) ThreadLocalMap中的set()

    ThreadLocalMap使用开发地址-线性探测法来解决哈希冲突,线性探测法的地址增量di = 1, 2, ... 其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

    按照上面的描述,可以把table看成一个环形数组

    先看一下线性探测相关的代码,从中也可以看出来table实际是一个环:

    /**
         * 获取环形数组的下一个索引
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }
    
        /**
         * 获取环形数组的上一个索引
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

    ThreadLocalMap的set()代码如下:

    private void set(ThreadLocal<?> key, Object value) {
            ThreadLocal.ThreadLocalMap.Entry[] tab = table;
            int len = tab.length;
            //计算索引,上面已经有说过。
            int i = key.threadLocalHashCode & (len-1);
    
            /**
             * 根据获取到的索引进行循环,如果当前索引上的table[i]不为空,在没有return的情况下,
             * 就使用nextIndex()获取下一个(上面提到到线性探测法)。
             */
            for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //table[i]上key不为空,并且和当前key相同,更新value
                if (k == key) {
                    e.value = value;
                    return;
                }
                /**
                 * table[i]上的key为空,说明被回收了
                 * 这个时候说明改table[i]可以重新使用,用新的key-value将其替换,并删除其他无效的entry
                 */
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

    3.3内存泄漏

     每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.

      所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。  

      PS.Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。 

    synchronized是用时间换空间(牺牲时间)、ThreadLocal是用空间换时间(牺牲空间),为什么这么说?
    因为synchronized操作数据,只需要在主存存一个变量即可,就阻塞等共享变量,而ThreadLocal是每个线程都创建一块小的堆工作内存。显然,印证了上面的说法。
     
    一个线程对应一块工作内存,线程可以存储多个ThreadLocal。那么假设,开启1万个线程,每个线程创建1万个ThreadLocal,也就是每个线程维护1万个ThreadLocal小内存空间,而且当线程执行结束以后,假设这些ThreadLocal里的Entry还不会被回收,那么将很容易导致堆内存溢出。
     
    怎么办?难道JVM就没有提供什么解决方案吗?
    ThreadLocal当然有想到,所以他们把ThreadLocal里的Entry设置为弱引用,当垃圾回收的时候,回收ThreadLocal。
    什么是弱引用?
    1. Key使用强引用:也就是上述说的情况,引用ThreadLocal的对象被回收了,ThreadLocal的引用ThreadLocalMap的Key为强引用并没有被回收,如果不手动回收的话,ThreadLocal将不会回收那么将导致内存泄漏。
    2. Key使用弱引用:引用的ThreadLocal的对象被回收了,ThreadLocal的引用ThreadLocalMap的Key为弱引用,如果内存回收,那么将ThreadLocalMap的Key将会被回收,ThreadLocal也将被回收。value在ThreadLocalMap调用get、set、remove的时候就会被清除
    3. 比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除
    那按你这么说,既然JVM有保障了,还有什么内存泄漏可言?
    ThreadLocalMap使用ThreadLocal对象作为弱引用,当垃圾回收的时候,ThreadLocalMap中Key将会被回收,也就是将Key设置为null的Entry。如果线程迟迟无法结束,也就是ThreadLocal对象将一直不会回收,回顾到上面存在很多线程+TheradLocal,那么也将导致内存泄漏。(内存泄露的重点)
     
    其实,在ThreadLocal中,当调用remove、get、set方法的时候,会清除为null的弱引用,也就是回收ThreadLocal。
     ThreadLocal提供一个线程(Thread)局部变量,访问到某个变量的每一个线程都拥有自己的局部变量。说白了,ThreadLocal就是想在多线程环境下去保证成员变量的安全。 

     

    弱引用:

     

  • 相关阅读:
    CF1539 VP 记录
    CF1529 VP 记录
    CF875C National Property 题解
    CF1545 比赛记录
    CF 1550 比赛记录
    CF1539E Game with Cards 题解
    CF1202F You Are Given Some Letters... 题解
    vmware Linux虚拟机挂载共享文件夹
    利用SOLR搭建企业搜索平台 之九(solr的查询语法)
    利用SOLR搭建企业搜索平台 之四(MultiCore)
  • 原文地址:https://www.cnblogs.com/dalianpai/p/12623823.html
Copyright © 2020-2023  润新知