• ThreadLocal = 本地线程?


    一、定义

    ThreadLocalJDK包提供的,从名字来看,ThreadLocal意思就是本地线程的意思。

    1.1 是什么?

    要想知道他是个啥,我们看看ThreadLocal的源码(基于JDK 1.8)中对这个类的介绍:

    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).
    

    大致能够总结出:

    1. TreadLocal可以给我们提供一个线程内的局部变量,而且这个变量与一般的变量还不同,它是每个线程独有的,与其他线程互不干扰的;
    2. ThreadLocal 与普通变量的区别在于:每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都会被回收;
    3. 简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,每个线程的数据不共享,自然就没有线程安全方面的问题了.

    1.2 示例

    一言不合上代码!

    //创建ThreadLocal变量
    private static ThreadLocal<String> localParam = new ThreadLocal<>();
    
    @Test
    public void threadLocalDemo() {
        //创建2个线程,分别设置不同的值
        new Thread(() -> {
            localParam.set("Hello 风尘博客!");
            //打印当前线程本地内存中的localParam变量的值
            log.info("{}:{}", Thread.currentThread().getName(), localParam.get());
        }, "T1").start();
        new Thread(() -> {
            log.info("{}:{}", Thread.currentThread().getName(), localParam.get());
        }, "T2").start();
    }
    
    • 结果:
    ... T1:Hello 风尘博客!
    ... T2:null
    

    打印结果证明,T1线程中设置的值无法在T2取出,证明变量ThreadLocal在各个线程中数据不共享。

    1.3 ThreadLocalAPI

    ThreadLocal定义了四个方法:

    1. get():返回此线程局部变量当前副本中的值;
    2. set(T value):将线程局部变量当前副本中的值设置为指定值;
    3. initialValue():返回此线程局部变量当前副本中的初始值;
    4. remove():移除此线程局部变量当前副本中的值。
    • set()initialValue()区别
    名称 set() initialValue()
    定义 为这个线程设置一个新值 该方法用于设置初始值,并且在调用get()方法时才会被触发,所以是懒加载。但是如果在get()之前进行了set()操作,这样就不会调用
    区别 如果对象生成的时机不由我们控制的时候使用 set() 方式 对象初始化的时机由我们控制的时候使用initialValue() 方式

    二、实现原理

    ThreadLocal有一个特别重要的静态内部类ThreadLocalMap,该类才是实现线程隔离机制的关键。

    • 每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面,也就是说:ThreadLocal类型的本地变量存放在具体的线程内存空间中。
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
    • Thread类中有两个ThreadLocalMap类型的变量,分别是threadLocalsinheritableThreadLocals,而ThreadLocalMap是一个定制化的Hashmap,专门用来存储线程本地变量。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocalset()或者get()方法时才会创建它们。

    风尘博客

    • ThreadLocal就是一个工具壳,它通过set()方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get()方法时,再从当前线程的threadLocals变量里面将其拿出来使用。

    • 如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove()方法,从当前线程的threadLocals里面删除该本地变量。

    另外Thread里面的threadLocals被设计为Map结构是因为每个线程可以关联多个ThreadLocal变量。

    原理小结

    1. 每个Thread维护着一个ThreadLocalMap的引用;
    2. ThreadLocalMapThreadLocal的内部类,用Entry来进行存储;
    3. 调用ThreadLocalset()方法时,实际上就是往ThreadLocalMap设置值,keyThreadLocal对象,值是传递进来的对象;
    4. 调用ThreadLocalget()方法时,实际上就是往ThreadLocalMap获取值,keyThreadLocal对象;
    5. ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value

    三、使用场景

    3.1 ThreadLocal的作用

    • 保存线程上下文信息,在任意需要的地方可以获取.

    由于ThreadLocal的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。

    • 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失.

    3.2 场景一:独享对象

    每个线程需要一个独享对象(通常是工具类,典型需要使用的类有SimpleDateFormatRandom

    这类场景阿里规范里面也提到了:

    风尘博客

    3.3 场景二:当前信息需要被线程内的所有方法共享

    每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。

    演示(完整演示见文末Github

    • User.java
    @Data
    public class User {
        private String userName;
    
        public User() {
    
        }
    
        public User(String userName) {
            this.userName = userName;
        }
    }
    
    • UserContextHolder.java
    public class UserContextHolder {
    
        public static ThreadLocal<User> holder = new ThreadLocal<>();
    
    }
    
    • Service1.java
    public class Service1 {
    
        public void process() {
            User user = new User("Van");
            //将User对象存储到 holder 中
            UserContextHolder.holder.set(user);
            new Service2().process();
        }
    }
    
    • Service2.java
    public class Service2 {
    
        public void process() {
            User user = UserContextHolder.holder.get();
            System.out.println("Service2拿到用户名: " + user.getUserName());
            new Service3().process();
        }
    }
    
    • Service3.java
    public class Service3 {
    
        public void process() {
            User user = UserContextHolder.holder.get();
            System.out.println("Service3拿到用户名: " + user.getUserName());
        }
    }
    
    • 测试方法
    @Test
    public void threadForParams() {
        new Service1().process();
    }
    
    • 结果打印
    Service2拿到用户名: Van
    Service3拿到用户名: Van
    

    3.4 使用ThreadLocal的好处

    1. 达到线程安全的目的;
    2. 不需要加锁,执行效率高;
    3. 更加节省内存,节省开销;
    4. 免去传参的繁琐,降低代码耦合度。

    四、问题

    4.1 内存泄漏问题

    内存泄露:某个对象不会再被使用,但是该对象的内存却无法被收回

    • 正常情况

    Thread运行结束后,ThreadLocal中的value会被回收,因为没有任何强引用了。

    • 非正常情况

    Thread一直在运行始终不结束,强引用就不会被回收,存在以下调用链

    Thread-->ThreadLocalMap-->Entry(key为null)-->value
    

    因为调用链中的 valueThread 存在强引用,所以value无法被回收,就有可能出现OOM

    如何避免内存泄漏(阿里规范)

    调用remove()方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal后,要调用remove()方法。

    4.2 ThreadLocal的空指针问题

    • ThreadLocalNPE.java
    public class ThreadLocalNPE {
    
        ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
    
        public void set() {
            longThreadLocal.set(Thread.currentThread().getId());
        }
    
        /**
         * 当前返回值为基本类型,会报空指针异常,如果改成包装类型Long就不会出错
         * @return
         */
        public long get() {
            return longThreadLocal.get();
        }
    }
    
    • 空指针测试
    @Test
    public void threadLocalNPE() {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        //如果get方法返回值为基本类型,则会报空指针异常,如果是包装类型就不会出错
        System.out.println(threadLocalNPE.get());
    }
    

    如果get()方法返回值为基本类型,则会报空指针异常;如果是包装类型就不会出错。这是因为基本类型和包装类型存在装箱和拆箱的关系,所以,我们必须将get()方法返回值使用包装类型。

    4.3 参考文章

    1. 再也不学Threadlocal了,看这一篇就忘不掉了(万字总结)
    2. 使用 ThreadLocal 一次解决老大难问题

    四、技术交流

    Github 示例代码

    1. 风尘博客:https://www.dustyblog.cn
    2. 风尘博客-掘金
    3. 风尘博客-博客园
    4. Github
  • 相关阅读:
    Java时间转换的一个特性
    JS处理数据四舍五入
    DataReader分页性能测试
    Java通过cal.get(Calendar.MONTH)比真实月份少一个月
    sqlserver split函数
    一个CLR20r3 错误解决。
    Devexpress dll搜集
    正则表达式:小括号、中括号、大括号的区别
    Android手机有的不显示Toast
    保存页面数据的场所----Hidden、ViewState、ControlState
  • 原文地址:https://www.cnblogs.com/vandusty/p/12194718.html
Copyright © 2020-2023  润新知