1.什么是ThreadLocal2.ThreadLocal能做什么?什么是ThreadLocal在开始了解ThreadLocal是什么之前,我们可以在java中搜索这个类,看源码注释:意思大概是:ThreadLocal提供了线程私有的局部变量,不同于普通变量,因为每个每个访问ThreadLocal实例(通过get或set方法)的线程都有自己的、独立初始化的副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是将状态(用户id或事务id)与线程相关联。2.ThreadLocal能做什么听到上面的描述你可能还是一头雾水。简单来说,ThreadLocal可以做这样的事情:它实现了每个线程都有自己独占的局部变量副本(使用自己的变量,不打扰别人,不与别人共享,每个人都有一份)。主要解决让每个线程绑定自己的值的问题,通过使用get()和set()方法获取默认值或者将其值更改为当前线程中保存的副本的值来避免线程安全问题.3.ThreadLocal在生产中的应用通过上面的讲解,我们应该明白了ThreadLocal是什么以及怎么用了,但是我们还是不知道怎么用,一时半会儿也不会想到合适的使用场景(一些在多线程运行时需要每人一份的场景是什么?)这时候我们举个例子,马上就能明白它的使用场景:我们的切入点是——SimpleDateFormat对于SimpleDateFormat,我们应该非常熟悉它。耳熟能详,我们平时就是用这个工具类来转换日期格式的,貌似没什么问题,但是使用不慎,就会出大问题!我们看一下SimpleDateFormat的源码注释:SimpleDateFormatisnotthread-safe。建议为每个线程创建一个独立的格式实例。如果多个线程同时访问一个格式,则必须在外部进行同步。假设我们在工作的时候经常用到SimpleDateForma,我们将其独立提取为一个工具类,也许我们会新建一个SimpleDateForma对象,然后再复用这个对象,殊不知,这种方式多线程的危险性写性!publicclassDateUtils{//先新建一个日期转换类publicstaticfinalSimpleDateFormatsdf=newSimpleDateFormat("yyyy-MM-ddHH:mm:ss");/***在并发环境下使用SimpleDateFormat的parse方法解析字符ConvertstringtoDateobject**@paramstringDate*@return*@throwsException*/publicstaticDateparseDate(StringstringDate)throwsException{returnsdf.解析(字符串日期);}publicstaticvoidmain(String[]args)throwsException{for(inti=1;i<30;i++){newThread(()->{try{System.out.println(DateUtils.parseDate("2020-11-1111:11:11"));}catch(Exceptione){e.printStackTrace();}},String.valueOf(i)).start();}}}就像上面的代码,在多线程并发的情况下,使用同一个工具类,会出什么问题呢?运行代码看看:由此可见多线程下操作日期转换工具类很容易出bug原因是:SimpleDateFormat类内部有一个Calendar对象引用,用来存储当前SimpleDateFormat对象相关的日期信息,这会造成一个问题:如果多个线程引用这个SimpleDateFormat,那么多个线程会共享这个类,还要分享这个Calendar引用,那么可能这个线程没有转换完成日期,这个日期会被替换,会出现各种并发操作。针对以上情况,我们有以下四种方法来解决这个问题:1)将SimpleDateFormat定义为局部变量。缺点:每次调用转换方法都会创建一个SimpleDateFormat对象,方法结束后会被垃圾回收。2)锁定。在该方法中加入同步序列化会严重影响吞吐量。3)使用其他工具阿里巴巴开发手册松山版指出:4)就是我们今天要介绍的,使用ThreadLocal.publicclassDateUtils{//publicstaticfinalSimpleDateFormatsdf=newSimpleDateFormat("yyyy-MM-ddHH:mm:ss");publicstaticfinalThreadLocalSIMPLE_DATE_FORMAT_THREAD_LOCAL=ThreadLocal.withInitial(()->newSimpleDateFormat("yyyy-MM-ddHH:mm:ss"));/***模拟并发环境下使用SimpleDateFormat的parse方法将字符串转为Date对象**@paramstringDate*@return*@throwsException*/publicstaticDateparseDate(StringstringDate)throwsException{returnSIMPLE_DATE_FORMAT_THREAD_LOCAL.get().parse(stringDate);}publicstaticvoidmain(String[]args)throwsException{for(inti=1;i<30;i++){newThread(()->{try{System.out.println(DateUtils.parseDate("2020-11-1111:11:11"));}catch(Exceptione){e.printStackTrace();}},String.valueOf(i)).start();这样每个调用它的线程都使用工具类的一份副本,每个线程一个,所以不会出现这样的问题4.ThreadLocal源码分析那么ThreadLocal是怎么做到的,每个Thread都有一个变量的副本?我们来分析一下源码:我们发现Thread对象中的ThreadLocal对象中有一个静态类ThreadLocalMap。而ThreadLocalMap是由Entry(ThreadLocal>k,Objectv)组成的。由此我们可以得到如下图:threadLocalMap其实是一个以ThreadLocal为键,Object为值的对象。当我们给threadLocalMap变量赋值时,当前的ThreadLocal值就是key,传入的值是一个Etry,存放在这个threadLocalMap中。大概的理解是:1)每个Thread都有自己的ThreadLocal。2)每个Thread都有一个ThreadLocalMap,里面存储了一个键值对,ThreadLocal作为键,传入的值作为值。3)当每个线程需要使用ThreadLocal时,使用当前线程从Map中获取,这样每个线程都有自己独立的变量。4)人人有一份,完全杜绝了竞争条件,是并发模式下绝对安全的变量。5.ThreadLocal内存泄漏问题其实ThreadLocal存在一些问题:内存泄漏问题。什么是内存泄漏:不再使用的对象或变量占用的内存无法回收,这就是内存泄漏。在解释这个现象之前,我们先看一下继承了WeakReference类和弱引用的Entry。关于强虚引用和弱虚引用,我们在之前的博客中已经介绍过了。深入理解JVM(八)——强引用和弱引用强引用:当内存不足时,JVM启动垃圾回收。对于强引用的对象,即使发生OOM,对象也不会被回收。软引用:如果内存足够,对象不会被回收。在内存不足的前提下,回收对象的弱引用:不管内存是否充足,只要是弱引用,都会被回收。Phantomreference:**当这个对象被收集器回收时,它会收到系统通知或在序列中添加进一步的处理。它由jvm技术人员使用。**为什么Entry使用弱引用?每当执行一个函数,栈帧就被销毁,此时ThreadLocal已经没有了,但是此时线程的ThreadLocalMap中的一个entry的key引用仍然指向这个对象。weakkey是强引用,会导致key指向的ThreadLocal对象和v指向的对象不被gc回收,导致内存泄漏。weakkey就是弱引用,可以大概率的减少内存泄露的问题(也有key为null的问题)。使用弱引用可以将键引用指向null。由此可见,通过弱引用,确实可以在一定程度上解决内存泄漏的问题。但是在gc完成后,ThreadLocalMap会有一个key为null的Entry,而这些key为null的entry的值是没有办法访问的。如果当前线程长时间没有结束(比如线程池的常驻核心线程),这些键为null。入口总会有很强的引用链,价值可能会超大,进一步拖累服务器的性能。所以,弱引用并不能100%保证不会泄露内存。当我们不使用一个ThreadLocal对象后,我们需要手动调用remoev方法删除它,尤其是在线程池中,不仅仅是内存泄漏的问题,因为线程池中的线程是被复用的,也就是说线程该线程的ThreadLocalMap对象也被重用。如果我们不手动调用remove方法,那么后面的线程可能会获取到前面线程留下的值,造成bug。上面的方法就是用来解决nullkey的内存泄漏问题的。另外,set()方法:get()方法:都做空key判断6.总结1)ThreadLocal没有解决线程间共享数据的问题2)ThreadLocal适用于线程间变量隔离共享的场景方法之间3)ThreadLocal在不同的县隐式创建独立的实例副本,避免了实例线程安全的问题4)每个线程持有一个专用于自己的Map,维护ThreadLocal对象与具体实例之间的映射。(map只能自己访问,所以不存在线程安全问题)5)ThreadLocalMap的ThreadLocalEntry对ThreadLocal有一个弱引用,避免了ThreadLocal对象不能被回收的问题。6)通过expungeStaleEntry、cleanSomeSlots、replaceStaleEntry这三个方法回收key为null的Entry对象(即具体实例)的value和Entry对象本身,防止内存泄露,是一种安全的方法加强