• 【JUC】ThreadLocal


    1. 概述

    ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

    2. 使用

    阿里规范:

    【强制】必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代码中使用try-finally块进行回收

    public class Temp {
    
        ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);
    
        public void add() {
            threadLocalField.set(1 + threadLocalField.get());
        }
    
        public static void main(String[] args) throws InterruptedException {
            Temp temp = new Temp();
            ExecutorService executorService = Executors.newFixedThreadPool(3);
    
            for (int i = 0; i < 10; i++) {
                executorService.submit(() -> {
                    try {
                        Integer beforeInt = temp.threadLocalField.get();
                        temp.add();
                        Integer afterInt = temp.threadLocalField.get();
                        System.out.println(Thread.currentThread().getName()+"\tbeforeInt:" + beforeInt + "\tafterInt:" + afterInt);
                    } finally {
    //                    temp.threadLocalField.remove();
                    }
    
                });
            }
    
            executorService.shutdown();
        }
    }
    
    • 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

    不加temp.threadLocalField.remove(),输出:业务逻辑受到了污染

    pool-1-thread-2	beforeInt:0	afterInt:1
    pool-1-thread-1	beforeInt:0	afterInt:1
    pool-1-thread-3	beforeInt:0	afterInt:1
    pool-1-thread-2	beforeInt:1	afterInt:2
    pool-1-thread-1	beforeInt:1	afterInt:2
    pool-1-thread-3	beforeInt:1	afterInt:2
    pool-1-thread-2	beforeInt:2	afterInt:3
    pool-1-thread-3	beforeInt:2	afterInt:3
    pool-1-thread-1	beforeInt:2	afterInt:3
    pool-1-thread-2	beforeInt:3	afterInt:4
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    加了temp.threadLocalField.remove()正确输出

    pool-1-thread-1	beforeInt:0	afterInt:1
    pool-1-thread-3	beforeInt:0	afterInt:1
    pool-1-thread-2	beforeInt:0	afterInt:1
    pool-1-thread-1	beforeInt:0	afterInt:1
    pool-1-thread-3	beforeInt:0	afterInt:1
    pool-1-thread-2	beforeInt:0	afterInt:1
    pool-1-thread-3	beforeInt:0	afterInt:1
    pool-1-thread-1	beforeInt:0	afterInt:1
    pool-1-thread-2	beforeInt:0	afterInt:1
    pool-1-thread-3	beforeInt:0	afterInt:1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3. Thread、ThreadLocal和ThreadLocalMap

    在这里插入图片描述

    近似的可以理解为:

    ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包的ThreadLocal对象:

    JVM内部维护了一个线程版的Map(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取的变量,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。

    ThredLocal是一个壳子,真正的存储结构是ThreadLocal里有ThreadLocalMap这么个内部类,每个Thread对象维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。

    1. 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
    2. 调用ThreadLocal的get0)方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象ThreadLocal本身并不存储值(ThreadLocal是一个壳子),它只是自己作为一个key来让线程从ThreadLocalMap获取value。正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响

    4. 内存泄露问题

        public static void main(String[] args) {
            ThreadLocal<String> t1 = new ThreadLocal<>();   // line1
            t1.set("老铁");                                  // line2
            t1.get();                                       // line3
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    line1新建了一个ThreadLocal对象,t1是强引用指向这个对象

    line2调用set()方法后新建一个Entry,通过源码可知Entry对象里的key是弱引用指向这个对象

    在这里插入图片描述

    4.1 为什么源代码用弱引用?

    当main()方法执行完毕后,栈帧销毁强引用t1也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;

    若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷,第2个坑后面讲)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null

    4.2 内存泄露再分析

    1. 当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(t1=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收。这样一来,ThreadLocaMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链: Thread Ref>Thread -> ThreaLocalMap ->Entry-value永远无法回收,造成内存泄漏

    2. 当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收

    3. 但在实际使用中我们大部分会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用,线程是不会结束的,所以threadLocal内存泄漏就值得我们小心

    最后分析方法,在set(),get(),remove()方法中,在threadLocal的生命周期里,针对threadLocal存在的内存泄露的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。故使用threadLocal时应尽量在代码中使用try-finally块调用remove进行回收

    5. 最佳实践

    1. ThreadLocal.withInitial(() -> 初始化值);
    2. 建议把ThreadLocal修饰为static
      • 【参考】ThreadLocal对象使用static修饰,ThreadLocal无法解决共享对象的更新问题
      • 说明:这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量
      • ThreadLocal能实现了线程的数据隔离,不在于它自己本身,而在于Thread的ThreadLocalMap,所以ThreadLocal可以只初始化一次,只分配一块存储空间就足以了,没必要作为成员变量多次被初始化
    3. 用完记得手动remove
    public class Temp {
    
        static ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);
    
        public void add() {
            threadLocalField.set(1 + threadLocalField.get());
        }
    
        public static void main(String[] args) throws InterruptedException {
            Temp temp = new Temp();
            ExecutorService executorService = Executors.newFixedThreadPool(3);
    
            for (int i = 0; i < 10; i++) {
                executorService.submit(() -> {
                    try {
                        Integer beforeInt = threadLocalField.get();
                        temp.add();
                        Integer afterInt = threadLocalField.get();
                        System.out.println(Thread.currentThread().getName()+"\tbeforeInt:" + beforeInt + "\tafterInt:" + afterInt);
                    } finally {
                        threadLocalField.remove();
                    }
    
                });
            }
    
            executorService.shutdown();
        }
    }
    
    • 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

    6. 小总结

    • ThreadLocal 并不解决线程间共享数据的问题

    • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景

    • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题

    • 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题

    • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题

    • 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为 null 的 Entry对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法

  • 相关阅读:
    超短高手赚大钱的必备3个问题
    html学习笔记
    【异常错误】torch.cuda.is_available()一直是false
    Zemax操作37--更换玻璃和非球面
    Mybatis之动态SQL
    计算机网络中常见缩略词翻译及简明释要
    strncpy很危险,但是为什么VS2005还支持它?
    JavaScript系列之Promise的resolve、reject、then、catch
    Kotlin 协程调度切换线程是时候解开谜团了
    Docker 部署本地爬虫项目到服务器
  • 原文地址:https://blog.csdn.net/xxx1276063856/article/details/133852397