美文网首页Java 程序员Java
ThreadLocal的源码解析以及内存泄漏的原理分析

ThreadLocal的源码解析以及内存泄漏的原理分析

作者: 程序花生 | 来源:发表于2021-10-21 17:03 被阅读0次

    介绍了Java中的ThreadLocal的作用、原理、源码以及应用,并且介绍了ThreadLocal的内存泄漏的原理以及解决办法。

    1 ThreadLocal的概述

    1.1 ThreadLocal的入门

    public class ThreadLocal< T > extends Object

    ThreadLocal来自JDK1.2,位于java.lang包。ThreadLocal可以提供线程内的局部变量,这种变量在线程的生命周期内起作用,ThreadLocal又叫做线程本地变量/线程本地存储。

    实际上,单就ThreadLocal这个类来说,它不存储任何内容,真正存储数据的集合在每个Thread中的threadLocals变量里面,ThreadLocal中只是定义了这个集合的结构,并提供了一系列操作的方法。后面的源码分析处会讲到!

    可以说,ThreadLocal只是一个工具类,一个对各个线程的threadLocals进行操作的工具而已。

    ThreadLocal 的作用和目的:

    1. 用于实现线程内的数据共享,某些数据是以线程为作用域并且不同线程具有不同的数据副本时,即数据在线程之间隔离,就可以考虑用ThreadLocal。即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
    2. 方便同一个线程复杂逻辑下的数据传递,有些时候一个线程中的任务过于复杂,我们又需要某个数据能够贯穿整个线程的执行过程,可能涉及到不同类/函数之间数据的传递。此时使用Threadlocal存放数据,在线程内部只要通过get方法就可以获取到在该线程中存进去的数据,方便快捷。

    1.2 同步和ThreadLocal

    同步与ThreadLocal是解决多线程中数据访问问题的两种思路,前者是数据共享的思路,后者是数据隔离的思路。同步是一种以时间换空间的思想,ThreadLocal是一种空间换时间的思想。前者仅提供一份变量,让不同的线程排队访问,实现串行化;而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

    Threadlocal并不能代替同步,注意ThreadLocal不是用来解决共享对象的多线程访问问题的。通过ThreadLocal的set()方法设置到线程的threadLocals里的是线程自己要存储的对象,其他线程不需要去访问,也是访问不到的。各个线程中的threadLocals以及里面的值都是不同的对象。Threadloocal是用来进行变量隔离,就是说ThreadLocal是针对那些不需要共享的属性!

    1.3 主要API方法与使用案例

    ThreadLocal类主要有四个可供调用的方法:

    1. void set(T value):保存值;
    2. T get():获取值;
    3. void remove():移除值;
    4. initialValue():返回该线程局部变量的初始值,该方法是为了让子类继承而设计的。这个方法是一个延迟调用方法,在一个线程第一次调用get()时(并且set未被调用)才执行。ThreadLocal中的默认实现是直接返回一个null。

    ThreadLocal实现线程内数据共享,线程间数据隔离的案例:

    public class ThreadLocalTest {
        /**
         * 全局ThreadLocal对象位于堆中,这是线程共享的,而方法栈,是每个线程私有的
         */
        static ThreadLocal<String> th = new ThreadLocal<>();
    
        public static void set() {
            //设置值,值为当前线程的名字
            th.set(Thread.currentThread().getName());
        }
    
        public static String get() {
            //获取值
            return th.get();
        }
    
        public static void main(String[] args) throws InterruptedException {
            System.out.println("主线程中尝试获取值:" + get());
            //主线程中设置值,值为线程名字
            set();
            //主线程中尝试获取值
            System.out.println("主线程中再次尝试获取值:" + get());
            //开启一条子线程
            Thread thread = new Thread(new Th1(), "child");
            thread.start();
            //主线程等待子线程执行完毕
            thread.join();
            System.out.println("等待子线程执行完毕,主线程中再次尝试获取值:" + get());
        }
    
        static class Th1 implements Runnable {
            @Override
            public void run() {
                System.out.println("子线程中尝试获取值:" + get());
                //子线程中设置值,值为线程名字
                set();
                System.out.println("子线程中再次尝试获取值:" + get());
            }
        }
    }
    

    结果如下:

    主线程中尝试获取值:null
    主线程中再次尝试获取值:main
    子线程中尝试获取值:null
    子线程中再次尝试获取值:child
    等待子线程执行完毕,主线程中再次尝试获取值:main
    

    先设置值,然后获取,可以得到“main”。然后开启子线程,在子线程内部,先获取,得到null,然后设置值,再获取,得到“child”。最后在主线程中再尝试获取,得到的还是原值“main”,这说明ThreadLocal使得变量的作用范围限制在本线程中了,其他线程是无法访问到该变量的。

    注意这里由于案例演示在最后并没有调用remove方法,在实际使用中应该在使用完毕之后调用remove方法,原理后面会讲!

    2 ThreadLocal的原理

    2.1 基本关系

    ThreadLocal类中定义了一个内部类ThreadLocalMap,ThreadLocalMap是真正存放数据的容器,实际上它的底层就是一张哈希表。

    每个Thread线程内部都定义有一个ThreadLocal.ThreadLocalMap类型的threadLocals变量,这样,线程之间的ThreadLocalMap互不干扰。threadLocals变量持有的ThreadLocalMap在ThreadLocal调用set或者get方法时才会初始化。

    ThreadLocal还提供相关方法,负责向当前线程的ThreadLocalMap变量获取和设置线程的变量值,相当于一个工具类。

    当在某个线程的方法中使用ThreadLocal设置值的时候,就会将该ThreadLocal对象添加到该线程内部的ThreadLocalMap中,其中键就是该ThreadLocal对象,值可以是任意类型任意值。当在某个线程的方法中使用ThreadLocal获取值的时候,会以该ThreadLocal对象为键,在该线程的ThreadLocalMap中获取对应的值。

    ThreadLocal中定义了ThreadLocalMap的结构,并提供操作的方法:

        public class ThreadLocal<T> {
            //……
            static class ThreadLocalMap {
                //……
            }
    
            /**
             * ThreadLocal的构造器,里面什么都没有
             * 创建ThreadLocal时,没有初始化ThreadLocalMap,在set、get方法中还可能初始化!
             */
            public ThreadLocal() {
            }
        }
    

    每个thread对象都持有一个ThreadLocalMap类型的引用变量,用于存放线程本地变量。key为ThreadLocal对象,value为要存储的数据。

    public class Thread implements Runnable {
        /*与此线程相关的线程本地值。此ThreadLocalMap定义在ThreadLocal类中,使用在Thread类中*/
        ThreadLocal.ThreadLocalMap threadLocals = null;
        //………………
    }
    

    下面是Thread、threadlocalMap、ThreadLocal的关系图:

    2.2 基本结构

    ThreadLocal中定义了ThreadLocalMap的结构。

    ThreadLocalMap也是一张key-value类型的哈希表,但是ThreadLocalMap并没有实现Map接口,它内部具有一个Entry类型的table数组用于存放节点。Entry节点用于存放key、value数据,并且继承了WeakReference。

    通过对该 ThreadLocal 对象进行哈希运算,可以得到该 ThreadLocal 对象在 Entry 数组中的桶位,从而找到唯一的 Entry。**如果发生了哈希冲突,那么与 HashMap 和 Hashtable 采用的“链地址法”不同,ThreadLocalMap 采用“线性探测法”解决哈希冲突,采用该方法的原因是实现很简单,占用更小的空间,并且一般来说一个ThreadLocalMap并不会存放很多数据!
    在创建ThreadLocalMap对象的同时即初始化16个长度的内部table数组,扩容阈值为len * 2 / 3,扩容增量为增加原容量的1倍

    在没有使用ThreadLocal设置、获取值时,线程中的ThreadLocalMap对象一直为null。

    /**
     * ThreadLocal的内部类ThreadLocalMap
     */
    static class ThreadLocalMap {
    
        /**
         * table数组的初始化容量,
         */
        private static final int INITIAL_CAPACITY = 16;
        //存放数据的数组,在创建ThreadLocalMap对象时将会初始化该数组,大小必须是2^N次方
        private Entry[] table;
        //扩容阈值,为len * 2 / 3
        private int threshold;
    
        /**
         * 内部节点对象,貌似没找到“key”字段在哪里,实际上存放k的属性位于其父类WeakReference的父类Reference中,名为referent,即属性复用
         * 插入数据时,通过对key(threadLocal对象)的hash计算,来找出Entry应该存放的table数组的桶位,
         * 不过可能造成hash冲突,它采用线性探测法解决冲突,因此需要线性向后查找。
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            //存放值
            Object value;
            //构造器
            Entry(ThreadLocal<?> k, Object v) {
                //调用父类的构造器,传入key,这里k被包装成为弱引用
                //实际上存放k的属性位于其父类WeakReference的父类Reference中,名为referent,即属性复用
                super(k);
                value = v;
            }
        }
    }
    

    2.3 set方法

    set方法是由ThreadLocal提供的,用于存放数据,大概步骤如下:

    1. 获取当前线程的成员变量threadLocals;
    2. 如果threadLocals不等于null,则调用set方法存放数据,方法结束;
    3. 否则,调用createMap方法初始化threadLocals,然后存放数据,方法结束。
    /**
     * ThreadLocal中的方法,开放给外部调用的存放数据的方法
     *
     * @param value 需要存放的数据
     */
    public void set(T value) {
        //注意,这里首先获取当前线程t
        Thread t = Thread.currentThread();
        //1.1 然后通过getMap方法,传入t,获取当前t线程的threadLocals
        ThreadLocalMap map = getMap(t);
        //如果map存在,则存放数据
        if (map != null)
            //this代指当前ThreadLocal对象,value表示值
            map.set(this, value);
        else
            //如果不存在,则构建属于当前线程的ThreadLocalMap并存放数据
            createMap(t, value);
    }
    
    /**
     * 1.1 ThreadLocal中的方法,获取指定线程的threadLocals
     *
     * @param t 指定线程
     * @return t的threadLocals
     */
    ThreadLocalMap getMap(Thread t) {
        //t代表当前线程,获取该线程的threadLocals属性,该属性就是一个ThreadLocalMap,默认为null
        return t.threadLocals;
    }
    
    /**
     * 1.2 ThreadLocal中的方法,用于构建ThreadLocalMap对象并赋值
     *
     * @param t          当前线程
     * @param firstValue 要存入的值
     */
    void createMap(Thread t, T firstValue) {
        //该方法是ThreadLocal中的方法,this代指当前ThreadLocal对象
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    /**
     * 位于ThreadLocalMap中的构造器,用于创建新的ThreadLocalMap对象
     *
     * @param firstKey   key
     * @param firstValue value
     */
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //创建table数组,初始容量为INITIAL_CAPACITY,即16
        table = new Entry[INITIAL_CAPACITY];
        //寻找数组桶位,通过ThreadLocal对象的threadLocalHashCode属性 & 15
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //该位置存放元素,由于是刚创建对象,因此不存在哈希冲突的情况,直接存储就行了
        //构造器在“基本结构”部分分析过,key最终被包装成弱引用。
        table[i] = new Entry(firstKey, firstValue);
        //size设置为1
        size = 1;
        //setThreshold方法设置扩容阀值
        setThreshold(INITIAL_CAPACITY);
    }
    
    /**
     * ThreadLocalMap中的方法,设置扩容阈值
     *
     * @param len 数组长度
     */
    private void setThreshold(int len) {
        //数组长度的2/3
        threshold = len * 2 / 3;
    }
    
    2.3.1 内部set方法

    上面的set方法中,如果当前t线程的threadLocals不为null,那么又调用了另一个私有的set方法存放数据。该方法是ThreadLocal的核心方法之一,并且比较复杂,大概具有如下步骤:

    1. 通过哈希算法计算出当前key存放的桶位i,并获取i的元素e。
    2. 如果e不为空,说明发生哈希冲突,使用线性探测法替换或者存放数据:
      1. 如果e的key和指定key相等(使用==比较),那么替换value,方法结束;
      2. 否则,如果e的key等于null,那说明是无效数据。调用replaceStaleEntry从该索引开始清理无效数据,并且存放新数据,在replaceStaleEntry过程中:
        1. 如果找到了key相等的entry,则它放到无效桶位中,value置为新值,方法结束。
        2. 如果没找到key相等的entry,直接在无效slot原地放entry,方法结束。
        3. 调用到了replaceStaleEntry方法,那就肯定能将新数据存入ThreadLocalMap中,并且不再执行后续步骤。
      3. 否则,nextIndex方法获取下一个索引并赋值给i,如果该位置的节点e为null,则结束循环,否则进行下一次循环;
    3. 走到这一步,说明没有替换value,也没有没有进行无效数据清理,而是找到了一个空桶位i,直接在该位置插入新entry,此时肯定保证最初始的i和现在的之间的位置都是存在有效节点的;
    4. 存放元素完毕之后,再调用cleanSomeSlots做一次部分无效节点清理,如果没清理出去key(返回false)并且当前table大小 大于等于 阈值,则调用rehash方法;
    5. rehash方法中会调用一次全表扫描清理的方法即expungeStaleEntries()方法。如果expungeStaleEntries完毕之后table大小还是大于等于(threshold – threshold / 4),则调用resize方法进行扩容;
    6. resize方法将扩容两倍,同时完成节点的转移。

    ThreadLocalMap使用==比较key是否相同。ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1(线性探测),寻找下一个相邻的位置。当向前寻找到数组头部或者向后寻找到数组尾部的时候,下一个位置就是数组尾部或者数组头部,即循环查找。

    /**
     * 位于ThreadLocalMap内的set方法,用于存放数据。
     *
     * @param key   ThreadLocal对象
     * @param value 值
     */
    private void set(ThreadLocal<?> key, Object value) {
        //tab保存数组引用
        Entry[] tab = table;
        //len保存数组的度
        int len = tab.length;
        /*1 哈希算法计算桶位   通过ThreadLocal的threadLocalHashCode属性计算出该key(ThreadLocal对象)对应的数组桶位i*/
        int i = key.threadLocalHashCode & (len - 1);
        /*
         *  2 使用线性探测法存放元素,可能进行垃圾清理
         * 获取i索引位置的Entry e,如果e不为null,说明发生了哈希冲突,下面开始解决:
         * 判断两个key是否相等,即是否需要进行value替换,如果相等,则替换value,解决完毕,方法返回;
         * 否则,判断获取的e的key是否为null,如果获取的ThreadLocal为null,这说明该WeakReference(弱引用)被回收了(因为Entry继承了WeakReference),
         * 说明ThreadLocal肯定在外部没有强引用了,这个Entry变成了垃圾,调用replaceStaleEntry方法擦除该位置或者其他的无效的Entry,重新赋值,解决完毕,方法返回,这是为了防止内存泄漏
         * 否则判断该位置i是否为null,即没有节点,如果为null,则在该位置新建节点并插入,解决完毕。
         * 否则,i = nextIndex(i, len),尝试下一次循环。
         * 如果循环完毕,方法还没结束,那说明没找到key相等的节点和key==null的节点,但是找到了下一个节点为null的桶位,记录此时索引值i,将会在该位置插入新节点。
         *
         * 这就是ThreadLocalMap解决哈希冲突的办法,即开放定址法——线性探测:当冲突时,向下查找下一个节点为null的位置存放新节点
         * nextIndex()方法用于循环数组索引,即如果初始i为15,长度为16,那么nextIndex将返回0,如果初始i为1,长度为16,那么nextIndex将返回2。
         * 这样的做法有利于利用起始索引前面的空间
         * */
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            /*获取该Entry的key,即原来的ThreadLocal对象,这是其父类Reference的方法*/
            ThreadLocal<?> k = e.get();
            /*如果获取的ThreadLocal和要存的ThreadLocal是同一个对象,那么就替换值,方法结束
             * 这里能够看出,判断key相等的条件是两个对象使用==比较返回true
             * */
            if (k == key) {
                e.value = value;
                return;
            }
            /*
             *如果获取的ThreadLocal为null,这说明该WeakReference(弱引用)被回收了(因为Entry继承了WeakReference),
             * 说明ThreadLocal肯定在外部没有强引用了,这个Entry变成了垃圾,擦除该位置的Entry,重新赋值并结束方法,这是为了防止内存泄漏*/
            if (k == null) {
                /*
                 * 从该位置开始,继续寻找key,并且会尽可能清理其他无效slot
                 * 在replaceStaleEntry过程中,如果找到了key,则做一个swap把它放到那个无效slot中,value置为新值
                 * 在replaceStaleEntry过程中,没有找到key,直接在该无效slot原地放entry
                 * */
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        /*
         * 执行到这一步方法还没有返回,说明i位置没有节点,此时e等于null,直接在该位置插入新的Entry
         * 此时肯定保证最初始的i和现在的之间的位置是存在节点的!
         * */
        tab[i] = new Entry(key, value);
        //size自增1,使用sz记录
        int sz = ++size;
        /* 3 尝试清理垃圾,然后判断是否需要扩容,如果需要那就扩容
         * 存放完毕元素之后,再调用cleanSomeSlots做一次垃圾清理,如果没清理出去key(返回false)
         * 并且当前table大小大于等于阈值,则调用rehash方法
         * rehash方法中会调用一次全量清理slot方法也即expungeStaleEntries()方法
         * 如果expungeStaleEntries完毕之后table大小还是大于等于(threshold – threshold / 4),则调用resize方法进行扩容
         * resize方法将扩容两倍,同时完成节点的转移
         * */
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    
    /**
     * 在length的索引范围内获取i的下一个索引,循环
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
    
    /**
     * 在length的索引范围内获取i的上一个索引,循环
     */
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
    
    private void rehash() {
        expungeStaleEntries();
        // Use lower threshold for doubling to avoid hysteresis
        if (size >= threshold - threshold / 4)
            resize();
    }
    
    2.3.2 哈希算法

    在set方法中,我们可以找到ThreadLocalMap的哈希算法为:

    int i = key.threadLocalHashCode & (len - 1);

    由于len长度一定是2的幂次方,因此上面的位运算可以转换为key.threadLocalHashCode% len,所以说ThreadLocalMap的哈希算法也是一种取模(求余)算法,因为余数一定会比除数小,那么计算出来的桶位肯定是位于[0, len-1]之间了,刚好在底层数组的索引范围内,还是比较简单的。

    这里的key我们知道是ThreadLocal对象,这个threadLocalHashCode属性看名字猜测就是该对象的哈希值了,那么这个值是通过hashCode方法得到的吗?实际上,threadLocalHashCode这个属性的得来非常的有趣,我们必须要去ThreadLocal源码中去看看!

    public class ThreadLocal<T> {
        /**
         * 下一个hashCode
         * 注意:这是个静态属性,那么只有在ThreadLocal的类第一次被加载进行类初始化的时候会被初始化,明显,初始化时为0。
         */
        private static AtomicInteger nextHashCode = new AtomicInteger();
    
        /**
         * threadlocal对象的hashcode,并非通过HashCode方法得到,他有自己的计算规则
         * 可以看到,它是调用nextHashCode()方法的返回值得来的
         */
        private final int threadLocalHashCode = nextHashCode();
    
        /**
         * 每个threadLocal对象通过该方法获取自己的hashcode
         */
        private static int nextHashCode() {
            //内部使用nextHashCode对象的getAndAdd方法
            //该方法首先返回当前的值,然后使得当前值的值加上指定的值,这里是HASH_INCREMENT
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
    
        /**
         * 哈希增量,顾名思义,就是哈希值的增量
         */
        private static final int HASH_INCREMENT = 0x61c88647;
    }
    

    结合上面的几个属性和方法,我们终于明白:

    在第一次创建ThreadLocal实例时,会加载ThreadLocal类,此时nextHashCode初始化值为0,然后是该对象threadLocalHashCode属性的初始化,在创建该类对象完毕之后,会自动调用nextHashCode方法,将此时nextHashCode的值作为自己的hashCode并且nextHashCode对象的值增加HASH_INCREMENT,明显是作为下一个ThreadLocal实例的hashCode值。

    即,每一个ThreadLocal实例使用创建该实例时的nextHashCode值作为自己的hashCode,然后将nextHashCode值增加HASH_INCREMENT,作为下一个ThreadLocal实例的hashCode。

    0x61c88647是十六进制的数,转换为十进制就是1640531527,实际上这个哈希增量的值的选取和斐波那契散列法、黄金比例有关

    2.4 get方法

    对于不同的线程,每次获取变量值时,是从本线程内部的threadLocals中获取的,别的线程并不能获取到当前线程的值,形成了变量的隔离,互不干扰。大概步骤如下:

    1. 获取当前线程的成员变量threadLocals
    2. 如果threadLocals非空,调用getEntry方法尝试查找并返回节点e:
      1. 如果e不为null,说明找到了,那爱么返回e的value,方法结束
      2. 如果e为null,说明没找到,方法继续。

    到这一步,说明可能是threadLocals为空,或者没找到e。那么调用setInitialValue方法,以当前ThreadLocal对象为key设置一个entry,并返回value。

    /**
     * ThreadLocal中的get方法,开放给外部调用
     *
     * @return 当前线程的当前ThreadLocal对象存入的值
     */
    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的threadLocals对象
        ThreadLocalMap map = getMap(t);
        //如果map不为null,即表示已经初始化过
        if (map != null) {
            //从map获取对应的Entry节点,传入this代表当前的ThreadLocal对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            //如果e不为null
            if (e != null) {
                //获取并返回值
                T result = (T) e.value;
                return result;
            }
        }
        //否则,如果map为null,或者e为null
        //那么返回null或者自定义的初始值
        return setInitialValue();
    }
    
    2.4.1 getEntry方法

    ThreadLocalMap内部的方法,根据key,尝试获取对应的Entry节点。 大概步骤如下:

    1. 根据key计算出桶位;
    2. 获取该桶位节点e,如果e不为null并且e的key和指定key相等(使用==比较),那么返回e,方法结束;
    3. 否则,调用getEntryAfterMiss方法进行一个步长的线性探测查找,查找过程中每碰到无效的节点,调用expungeStaleEntry进行清理;如果找到了则返回找到的entry;没有找到(探测到了空的桶位),则返回null。
    /**
     * ThreadLocalMap内部的方法,根据key,获取对应的Entry节点
     *
     * @param key key
     * @return Entry节点,没找到就返回null
     */
    private Entry getEntry(ThreadLocal<?> key) {
        //根据key计算桶位
        int i = key.threadLocalHashCode & (table.length - 1);
        //获取Entry节点e
        Entry e = table[i];
        /*如果e不为nul并且并且e内部key等于当前key(ThreadLocal对象)*/
        //可以看到key相等是使用==直接比较的
        if (e != null && e.get() == key)
            //则返回e
            return e;
        else
            /*否则使用线性探测查找
            线性探测查找过程中每碰到无效slot,调用expungeStaleEntry进行清理;如果找到了则返回entry;没有找到,返回null*/
            return getEntryAfterMiss(key, i, e);
    }
    
    2.4.2 setInitialValue方法

    ThreadLocal的方法,用于设置并返回初始值,在get方法没有找key对应的节点时,会调用该方法!

    大概有如下几步:

    1. 获取initialValue方法的返回值,作为新节点的value;
    2. 获取当前线程的ThreadLocalMap,判断是否为null;
    3. 如果不为null,则以当前ThreadLocal对象为key,存放value,方法结束;
    4. 如果为null,则初始化此线程的ThreadLocalMap,并以当前ThreadLocal对象为key,存放value,方法结束。
    /**
     * ThreadLocal的方法,设置并返回初始值
     *
     * @return 返回null或者通过initialValue方法用户自定义的初始值
     */
    private T setInitialValue() {
        //返回null或者用户重写该方法时自定义的返回值
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的map
        ThreadLocal.ThreadLocalMap map = getMap(t);
        //如果map不为null
        if (map != null)
            //尝试添加节点,以当前ThreadLocal对象为key,以null或者自定义的初始值为value
            map.set(this, value);
        else
            //否则初始化map并设置值
            createMap(t, value);
        //返回value,null或者自定义的初始值
        return value;
    }
    
    2.4.2.1 initialValue方法

    当get方法没有找到数据时,会调用setInitialValue方法,该方法中会调用initialValue方法,将默认返回null,用户也可以重写该方法,用于返回指定的值,相当于默认初始值。

    setInitialValue将会以当前ThreadLocal对象为key,initialValue的返回值为value,存放一个节点,同时返回value的值。

    /**
     * ThreadLocal的方法,默认返回空
     *
     * @return 默认返回null, 用户可以重写该方法返回自定义的初始值
     */
    protected T initialValue() {
        return null;
    }
    
    2.4.2.2 默认初始值案例
    public class ThreadLocalInitialValue {
        public static void main(String[] args) {
            //th1覆写了initialValue方法
            ThreadLocal th1 = new ThreadLocal() {
                @Override
                protected Object initialValue() {
                    return 11;
                }
            };
            //th2没有覆写了initialValue方法
            ThreadLocal th2 = new ThreadLocal();
            //由于并没有调用set方法设置数据,那么两个ThreadLocal的get方法都将不能找到存放的数据
            //此时th1将返回默认初始值,并设置key:th1 value:11
            System.out.println("th1初始值:" + th1.get());
            //th2将返回null,并设置key:th2 value:null
            System.out.println("th2初始值:" + th2.get());
        }
    }
    

    2.5 ThreadLocal的内存泄露

    2.5.1 内存泄漏的原理

    首先是基础知识,关于Java中的引用的介绍:Java中强、软、弱、虚四种对象引用的详解和案例演示

    根据上面的源码,我们知道在存放新结点时在Entry结点的构造器中,并不是直接使用ThreadLocal对象作为key的,而是使用由ThreadLocal对象包装成的弱引用对象作为Key的,key被弱引用的字段关联,获取key是也是从弱引用字段中获取的。

    为什么使用弱引用包装的ThreadLocal对象作为key? 因为如果某个entry直接使使用一个普通属性和ThreadLocal对象关联,即key是强引用。那么当最外面ThreadLocal对象的全局变量引用置空时,由于在ThreadLocalMap中存在key对这个ThreadLocal对象的强引用,那么这个ThreadLocal对象并不会被回收,但此时我们已经无法访问、利用这个对象,造成了key的内存泄漏。

    因此,ThreadLocal对象被包装为弱引用作为key。这样,当外部的ThreadLocal对象的强引用被清除时,由于在ThreadLocalMap中存储的是弱引用key,这个ThreadLocal对象只被弱引用对相关联,因此它就是一个弱引用对象,那么下一次GC时这个弱引用ThreadLocal对象可以自动被清除了。

    但是,此时仍然会造成内存泄漏,不过此时是value或者说Entry的内存泄漏。

    我们知道value是强引用。这就导致了一个问题,如果这个弱引用key被回收而变成null时,如果之前调用ThreadLocal方法设置值的线程一直持续运行,那么它的ThreadLocalMap也一直存在,那么内部的entry结点也一直存在,那么value肯定还存在,但是此时却不能通过key访问到了(因为key被回收变成null了),此时还是发生了内存泄露。

    所以说,最保险的办法是移除无效的Entry。

    2.5.2 如何避免内存泄漏

    我们在set和get方法的源码中能够看到,当遍历的entry的key为null时,此时将清除该entry,value置空,这样就可以解决部分内存泄漏问题。但这并不是绝对的,可能并没有遍历到key为null的entry时set、get方法就因为插入、获取成功而返回了,因此在set、get方法中,只会尝试将遍历的到无效数据清除,并且这种方式是一种被动的清除,不能即时清除无效数据。

    ThreadLocal还有一个remove方法,该方法可以将此ThreadLocal对象对应的entry清除。实际上,在对ThreadLocal的数据使用完毕之后,从逻辑上来说此时的entry就是无效的数据了,因此主动调用一次remove方法,将该entry移除。这样我们对使用完毕的entry进行手动清除,从根本上杜绝了内存泄漏问题。

    所以养成良好的编程习惯十分重要,使用完ThreadLocal的数据之后,一定要记得调用一次remove方法。

    3 总结与应用

    总结:

    1. 每个ThreadLocal由于实现线程本地存储,但是只能保存一个本地数据,如果想要一个线程能够保存多个数据,就需要创建多个ThreadLocal。
    2. ThreadLocalMap的key键为ThreadLocal包装成的弱引用,value为设置的值。ThreadLocal会有内存泄漏的风险,因此使用完毕必须手动调用remove清除!

    应用:

    1. 使用ThreadLocal的典型场景正如上面的数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。
    2. Spring MVC对于每一个请求线程的Request对象使用ThreadLocal属性封装到RequestContextHolder中,这样每条线程都能访问到自己的Request。
    3. Spring 声明式事务管理中,每一个事务的信息也是使用ThreadLocal属性封装到TransactionSynchronizationManager中的,以此实现不同线程的事务存储和隔离,以及事务的挂起和恢复。
    4. 原始的JDBC方式的时候可以使用ThreadLocal类来管理事务!

    作者:刘Java
    链接:https://juejin.cn/post/7021355637812494366
    来源:稀土掘金

    相关文章

      网友评论

        本文标题:ThreadLocal的源码解析以及内存泄漏的原理分析

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