• 十、ThreadLocal


    一、ThreadLocal简介

    1、概述

    1、JDK1.2提供,位于java.lang包,ThreadLocal可以提供线程内的局部变量,这种变量在线程的生命周期内起作用,ThreadLocal又叫做线程本地变量或线程本地存储
    2、实际上,就ThreadLocal这个类来讲,它不存储任何内容,真正存储数据的集合在每个Thread中的threadLocals变量里面,ThreadLocal中只是定义了这个集合的结构,并提供了一系列操作的方法。
    3、作用:
    • 用于实现线程内的数据共享,某些数据是以线程为作用域并且不同线程具有不同的数据副本时,即数据在线程之间隔离(避免了线程安全问题),就可以考虑用ThreadLocal。即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
    • 方便同一个线程复杂逻辑下的数据传递,有些时候一个线程中的任务过于复杂,我们又需要某个数据能够贯穿整个线程的执行过程,可能涉及到不同类/函数之间数据的传递。此时使用Threadlocal存放数据,在线程内部只要通过get方法就可以获取到在该线程中存进去的数据,方便快捷。

    2、常用方法

    方法说明
    public T get()返回当前线程的此线程局部变量副本中的值
    protected T initialValue()返回此线程局部变量的当前线程的“初始值”
    public void remove()删除此线程局部变量的当前线程值
    public void set(T value)将此线程局部变量的当前线程副本设置为指定值
    public static ThreadLocal withInitial(Supplier supplier)创建一个线程局部变量。变量的初始值是通过调用Supplier上的get方法来确定的。
    /**
     * @Date: 2022/8/14
     */
    public class ThreadLocalTest {
        public static void main(String[] args) {
            List<String> list = Arrays.asList("张三", "李四", "王五", "赵六", "陈七");
            User user = new User();
            for (int i = 0; i < 5; i++) {
                new Thread(() -> {
                    try {
                        int bonus = new Random().nextInt(5000) + 4000;
                        user.addSalary(bonus);
                        System.out.println(Thread.currentThread().getName() + " 的工资为:" + user.threadLocal.get());
                    } finally {
                        // 如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题
                        user.threadLocal.remove();
                    }
                }, list.get(i)).start();
            }
        }
    }
    
    class User {
        // 工资
        int salary;
    
        // 初始化threadLocal
        ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 3000);
    
        public void addSalary(int bonus) {
            // 获取
            Integer basicSalary = threadLocal.get();
            salary = basicSalary + bonus;
            // 设置
            threadLocal.set(salary);
        }
    }
    /**
     * 运行结果如下:
     * 赵六 的工资为:8361
     * 张三 的工资为:9083
     * 王五 的工资为:9843
     * 李四 的工资为:7778
     * 陈七 的工资为:8191
     */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    3、注意事项

    1、在阿里Java开发手册中提到:必须回收自定义的ThreadLocal变量,尤其是在线程池场景下,线程经常会被复用,如果不清理自定义的ThradLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题。尽量在代码中使用try-finally块进行回收,在finally中调用remove()方法。代码演示及运行结果如下:

    在这里插入图片描述

    2、解决方法:使用try-finally块进行回收,在finally中调用remove()方法,正确结果如下:

    在这里插入图片描述

    /**
     * @Author: ye.yanbin
     * @Date: 2022/8/14
     */
    public class ThreadLocalTest {
        public static void main(String[] args) {
            // 线程池
            ExecutorService threadPool = Executors.newFixedThreadPool(3);
    
            User user = new User();
            try {
                // 没有回收threadLocal写法
                // for (int i = 0; i < 5; i++) {
                //     threadPool.submit(() -> {
                //         int bonus = new Random().nextInt(5000) + 4000;
                //         System.out.println(Thread.currentThread().getName() + " 初始值为:" + user.threadLocal.get());
                //         user.addSalary(bonus);
                //         System.out.println(Thread.currentThread().getName() + " 计算后的值为:" + user.threadLocal.get());
                //     }, threadPool);
                // }
                
                // 回收threadLocal写法
                for (int i = 0; i < 5; i++) {
                    threadPool.submit(() -> {
                        try {
                            int bonus = new Random().nextInt(5000) + 4000;
                            System.out.println(Thread.currentThread().getName() + " 初始值为:" + user.threadLocal.get());
                            user.addSalary(bonus);
                            System.out.println(Thread.currentThread().getName() + " 计算后的值为:" + user.threadLocal.get());
                        } finally {
                            // 如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题
                            user.threadLocal.remove();
                        }
                    }, threadPool);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                threadPool.shutdown();
            }
        }
    }
    
    class User {
        // 工资
        int salary;
    
        // 初始化threadLocal
        ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 3000);
    
        public void addSalary(int bonus) {
            // 获取
            Integer basicSalary = threadLocal.get();
            salary = basicSalary + bonus;
            // 设置
            threadLocal.set(salary);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    二、ThreadLocal分析

    1、Thread、ThreadLocal、ThreadLocalMap三者关系

    1、每个Thread线程内部都定义有一个ThreadLocal.ThreadLocalMap类型的threadLocals变量,用于存放线程本地变量(key为ThreadLocal对象,value为要存储的数据),这样,线程之间的ThreadLocalMap互不干扰。threadLocals变量持有的ThreadLocalMap在ThreadLocal调用set或者get方法时才会初始化
    public class Thread implements Runnable {
        // 与此线程相关的线程本地值,此ThreadLocalMap定义在ThreadLocal类中,使用在Thread类中
        ThreadLocal.ThreadLocalMap threadLocals = null;
    }
    
    • 1
    • 2
    • 3
    • 4
    2、ThreadLocal类中定义了一个内部类ThreadLocalMap,ThreadLocalMap是真正存放数据的容器,实际上它的底层就是一张哈希表。
    3、ThreadLocal还提供相关方法,负责向当前线程的ThreadLocalMap变量获取和设置线程的变量值,相当于一个工具类。
    public class ThreadLocal<T> {
        static class ThreadLocalMap {
            //……
        }
    
        /**
         * ThreadLocal的构造器,里面什么都没有,创建ThreadLocal时,没有初始化ThreadLocalMap
         */
        public ThreadLocal() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    4、当在某个线程的方法中使用ThreadLocal设置值的时候,就会将该ThreadLocal对象添加到该线程内部的ThreadLocalMap中,其中键就是该ThreadLocal对象,值可以是任意类型任意值。当在某个线程的方法中使用ThreadLocal获取值的时候,会以该ThreadLocal对象为键,在该线程的ThreadLocalMap中获取对应的值
    5、三者关系图:

    在这里插入图片描述

    2、ThreadLocalMap源码

    1、ThreadLocalMap也是一张key-value类型的哈希表,但是ThreadLocalMap并没有实现Map接口,它内部具有一个Entry类型的table数组用于存放节点。Entry节点用于存放key、value数据,并且继承了WeakReference。
    2、在创建ThreadLocalMap对象的同时即初始化16个长度的内部table数组,扩容阈值为len * 2 / 3,扩容为原容量的2倍,在没有使用ThreadLocal设置、获取值时,线程中的ThreadLocalMap对象一直为null。
    static class ThreadLocalMap {
        /**
         * 内部节点对象,存放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被包装成为弱引用
                super(k);
                // 存放k的属性位于其父类WeakReference的父类Reference中,名为referent,即属性复用
                value = v;
            }
        }
    
        /**
         * table数组的初始化容量
         */
        private static final int INITIAL_CAPACITY = 16;
    
        /**
         * 存放数据的数组,在创建ThreadLocalMap对象时将会初始化该数组,大小必须是2^N次方
         */
        private Entry[] table;
    
        /**
         * table数组中元素个数
         */
        private int size = 0;
    
        /**
         * 扩容阈值,为len * 2 / 3
         */
        private int threshold; // Default to 0
    
        /**
         * 设置扩容大小阈值,为len * 2 / 3
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    3、ThreadLocal的set方法

    1、set方法是由ThreadLocal提供的,用于存放数据,大概步骤如下:
    • 获取当前线程的成员变量threadLocals
    • 如果threadLocals不等于null,则调用set方法存放数据,方法结束
    • 否则,调用createMap方法初始化threadLocals,然后存放数据,方法结束。
    /*
     * 将当前线程局部变量的当前线程副本设置为指定值
     */
    public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 通过getMap方法,获取当前线程t的threadLocals
        ThreadLocalMap map = getMap(t);
        // 如果threadLocals存在就存放数据,否则调用createMap方法初始化threadLocals
        if (map != null)
            // this代指当前ThreadLocal对象,value表示值
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    /*
     * 获取指定线程t的threadLocals属性,该属性就是一个ThreadLocalMap,默认为null
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    /*
     * 用于threadLocals初始化,创建一个ThreadLocalMap对象并赋值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    /*
     * 位于ThreadLocalMap中的构造器,用于创建新的ThreadLocalMap对象
     */
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        // 初始化table数组,容量为16
        table = new Entry[INITIAL_CAPACITY];
        // 寻找数组桶位,通过ThreadLocal对象的threadLocalHashCode属性 & 15
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        // 该位置存放元素,由于是刚创建对象,因此不存在哈希冲突的情况,直接存储就行了
        table[i] = new Entry(firstKey, firstValue);
        // size设置为1
        size = 1;
        // 调用setThreshold方法,设置扩容阀值
        setThreshold(INITIAL_CAPACITY);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    如果threadLocals不等于null,则调用ThreadLocalMap中的set方法存放数据
    /*
     * 位于ThreadLocalMap内的set方法,用于存放数据
     */
    private void set(ThreadLocal<?> key, Object value) {
    	// tab保存数组引用
        Entry[] tab = table;
        // 获取数组长度
        int len = tab.length;
        // 哈希算法计算桶位,通过ThreadLocal的threadLocalHashCode属性计算出该key(ThreadLocal对象)对应的数组桶位i(即存储位置的下标索引)
        int i = key.threadLocalHashCode & (len-1);
    
        // 获取i索引位置的Entry e,如果e不为null,说明发生了哈希冲突,使用线性探测法替换或者存放数据
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            // 获取该Entry的key,即原来的ThreadLocal对象,这是其父类Reference的方法
            ThreadLocal<?> k = e.get();
    		// 如果获取的ThreadLocal和要存的ThreadLocal是同一个对象,那么就替换值,方法结束
            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置为新值
                 * 如果没有找到key,直接在该无效slot位原地放entry
                 */
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    	/*
         * 执行到这一步方法还没有返回,说明i位置没有节点,此时e等于null,直接在该位置插入新的Entry
         * 此时肯定保证最初始的i和现在的之间的位置是存在节点的!
         */
        tab[i] = new Entry(key, value);
        // size自增1
        int sz = ++size;
        /*
         * 尝试清理垃圾,然后判断是否需要扩容,如果需要那就扩容
         * 存放完毕元素之后,再调用cleanSomeSlots做一次垃圾清理,如果没清理出去key(返回false)
         * 并且当前table大小大于等于阈值,则调用rehash方法
         * rehash方法中会调用一次全量清理slot方法也即expungeStaleEntries()方法
         * 如果expungeStaleEntries完毕之后table大小还是大于等于(threshold – threshold / 4),则调用resize方法进行扩容
         * resize方法将扩容两倍,同时完成节点的转移
         */
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            // 扩容
            rehash();
    }
    
    // 扩容
    private void rehash() {
        expungeStaleEntries();
        // Use lower threshold for doubling to avoid hysteresis
        if (size >= threshold - threshold / 4)
            resize();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    4、ThreadLocal的get方法

    1、对于不同的线程,每次获取变量值时,是从本线程内部的threadLocals中获取的,其他线程并不能获取到当前线程的值,形成了变量的隔离,互不干扰。大概步骤如下:
    • 获取当前线程的成员变量threadLocals
    • 如果threadLocals非空,调用getEntry方法尝试查找并返回节点e
    • 如果e不为null,说明找到了,那么返回e的value,方法结束
    • 如果e为null,说明没找到,方法继续。
    /*
     * 返回当前线程局部变量的当前线程副本中的值
     */
    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 通过getMap方法,获取当前线程t的threadLocals
        ThreadLocalMap map = getMap(t);
        // 如果threadLocals不为null,说明已经初始化过
        if (map != null) {
            // 从threadLocals获取对应的Entry节点,传入this代表当前的ThreadLocal对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 如果Entry节点不为null
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取Entry节点的值并返回
                T result = (T)e.value;
                return result;
            }
        }
        // 否则,如果threadLocals为null,或者Entry节点为null,那么返回null或者自定义的初始值
        return setInitialValue();
    }
    
    /*
     * 根据key,获取对应的Entry节点
     */
    private Entry getEntry(ThreadLocal<?> key) {
        // 根据key计算桶位(Entry在数组中的索引位置)
        int i = key.threadLocalHashCode & (table.length - 1);
        // 根据索引获取Entry节点e
        Entry e = table[i];
        // 如果e不为null,并且e内部key等于当前key(ThreadLocal对象),则返回
        if (e != null && e.get() == key)
            return e;
        else
            // 否则使用线性探测查找,线性探测查找过程中每碰到无效slot,调用expungeStaleEntry进行清理;
            // 如果找到了则返回entry;没有找到,返回null
            return getEntryAfterMiss(key, i, e);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    三、ThreadLocal的内存泄漏

    1、概述

    1、JVM垃圾回收的一些概念篇中提到过内存泄漏以及JVM可视化监控工具篇中也有对内存泄漏的补充说明
    2、JVM垃圾回收的一些概念篇中提到过强、弱、软、虚四种对象引用

    在这里插入图片描述

    2、为什么使用弱引用包装的ThreadLocal对象作为key

    1、如果某个Entry直接使用一个普通属性和ThreadLocal对象关联,即key是强引用。那么当最外面ThreadLocal对象的全局变量引用置空时,由于在ThreadLocalMap中存在key对这个ThreadLocal对象的强引用,那么这个ThreadLocal对象并不会被回收,但此时已经无法访问这个对象,就造成了key的内存泄漏
    2、因此ThreadLocal对象被包装为弱引用作为key。当外部的ThreadLocal对象的强引用被清除时,由于在ThreadLocalMap中存储的是弱引用key,这个ThreadLocal对象只被弱引用对相关联,因此它就是一个弱引用对象,那么下一次GC时这个弱引用ThreadLocal对象可以自动被清除了
    3、引发问题:
    • 由于Entry中的key(ThreadLocal对象)是弱引用,当外部的ThreadLocal对象的强引用被置为null时,那么系统GC时,根据可达性分析,这个ThreadLocal对象没有任何一条链路能够引用它,势必会被回收。
    • 这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如使用线程池,线程池中的线程会被复用),这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成value的内存泄漏。
    • 虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄漏。所以在不使用某个ThreadLocal对象后,要手动调用remove方法来删除它,尤其是在线程池中,不仅仅是内存泄漏的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug

    在这里插入图片描述

    4、从前面的set、getEntry、remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry

    3、总结

    1、ThreadLocal能实现线程的数据隔离,在于Thread的ThreadLocalMap,所以ThreadLocal可以只初始化一次,只分配一块内存空间即可,没必要作为成员变量多次被初始化,因此建议使用static修饰
    2、ThreadLocalMap的key为ThreadLocal包装成的弱引用,value为设置的值。ThreadLocal会有内存泄漏的风险,因此使用完毕必须手动调用remove清除
    3、应用场景:
    • 使用ThreadLocal的典型场景是数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。
    • Spring MVC对于每一个请求线程的Request对象使用ThreadLocal属性封装到RequestContextHolder中,这样每条线程都能访问到自己的Request。
    • Spring声明式事务管理中,每一个事务的信息也是使用ThreadLocal属性封装到TransactionSynchronizationManager中的,以此实现不同线程的事务存储和隔离,以及事务的挂起和恢复。
  • 相关阅读:
    Spring自动装配Bean
    算法通关村第六关|白银|二叉树的层次遍历【持续更新】
    java基础
    IP6510 为“快充”而生 支持PD及各种快充协议芯片多口快充解决方案
    ​Mysql数据库查询好慢,除了索引,还能因为什么?
    关于java中的static关键字
    Nginx
    ESP32+阿里云+vscode_Pio
    水质查询接口
    【AUTOSAR】【以太网】SomeIpXf
  • 原文地址:https://blog.csdn.net/qq_42200163/article/details/126474895