什么是ThreadLocal?从Java官方文档中的描述来看:ThreadLocal类用于提供线程内部的局部变量。在多线程环境下访问这个变量时(通过get和set方法访问),可以保证每个线程的变量相对独立于其他线程中的变量。ThreadLocal实例一般为私有静态类型,用于关联线程和线程上下文。我们可以知道,ThreadLocal的作用是提供线程中的局部变量,不同线程之间不会相互干扰。这个变量在线程的生命周期内起作用,减少了同一个线程中函数或组件的数量。一些公共变量传递的复杂性。线程并发:多线程并发场景下传递数据:我们可以使用ThreadLocal在同一个线程的不同组件之间传递公共变量(有点类似于Session?)线程隔离:各个线程的变量是独立的,不会相互影响基本使用在介绍ThreadLocal的使用之前,我们先了解一下ThreadLocal的一些常用方法和用例。下面我们来看下不安全线程的案例,感受一下ThreadLocal线程隔离的特点。/***需求:线程隔离*在多线程并发的场景下,每个线程中的变量是相互独立的*线程A:设置变量1,获取变量2*线程B:设置变量2,获取变量2*@author:Moxi*/publicclassMyDemo01{//变量私有字符串内容;publicStringgetContent(){返回内容;}publicvoidsetContent(Stringcontent){this.content=content;}publicstaticvoidmain(String[]args){MyDemo01myDemo01=newMyDemo01();for(inti=0;i<5;i++){newThread(()->{myDemo01.setContent(Thread.currentThread().getName()+"data");System.out.println("---------------------------------------");System.out.println(Thread.currentThread().getName()+"\t"+myDemo01.getContent());},String.valueOf(i)).start();}}}运行后的效果------------------------------------------------------------------------------------------------------------------34数据----------------------------------------24个数据--------------------------------------14data44data04data从上面我们可以看出有一个线程没有隔离的问题,就是线程1被取出来的内容线程4,那么如何解决呢?这时候就可以使用ThreadLocal了。我们通过set将变量绑定到当前线程,然后获取绑定到当前线程的变量/***需求:线程隔离*在多线程并发场景下,每个线程中的变量是相互独立的*线程A:设置变量1,获取变量2*ThreadB:设置变量2,获取变量2*@author:Moxi*/publicclassMyDemo01{//变量privateString内容;publicStringgetContent(){返回内容;}publicvoidsetContent(Stringcontent){这个。内容=内容;}publicstaticvoidmain(String[]args){MyDemo01myDemo01=newMyDemo01();ThreadLocalthreadLocal=newThreadLocal<>();for(inti=0;i<5;i++){newThread(()->{threadLocal.set(Thread.currentThread().getName()+"data");System.out.println("----------------------------------------");System.out.println(Thread.CcurrentThread().getName()+"\t"+threadLocal.get());},String.valueOf(i)).start();}}}引入ThreadLocal后,查看运行结果如下:---------------------------------------------------------------------------------4的数据4--------------------------------------33的数据---------------------------------------2的数据2----------------------------------------11data00data发现不会出现上面的情况,也就是当前线程只能获取线程线程存储对象ThreadLocal类和Synchronized关键字Synchronized同步方法对于上面的例子,这个功能可以通过加锁来实现。我们来看看使用同步代码块实现的效果:publicstaticvoidmain(String[]args){MyDemo03myDemo01=newMyDemo03();for(inti=0;i<5;i++){newThread(()->{synchronized(MyDemo03.class){myDemo01.setContent(Thread.currentThread().getName()+"data");系统。out.println("----------------------------------------");System.out.println(Thread.currentThread().getName()+"\t"+myDemo01.getContent());}},String.valueOf(i)).start();}}运行结果如下,发现通过加锁可以实现与ThreadLocal线程隔离的功能,但是并发度降低了----------------------------------------0数据为0-----------------------------------------44的数据------------------------------------------33的数据------------------------------------------2的数据2--------------------------------------11DataThreadLocal和Synchronized的区别虽然ThreadLocal模式和Synchronized关键字都是用来处理多线程并发访问变量的问题,但是两者处理问题的角度和思路是不同的。总结:刚才的案例,虽然ThreadLocal和Synchronized都可以解决问题,但是使用ThreadLocal更合适,因为它可以让程序有更高的并发。应用场景通过上面的介绍,我们已经基本了解了ThreadLocal的特点,但是它在什么场景下使用呢?接下来我们看一个案例:交易操作转账案例这里我们先搭建一个简单的转账场景:有一个数据表account,里面有两个用户jack和Rose,用户Jack给用户Rose转账。本案例的实现主要使用了mysql数据库、JDBC和C3P0框架。下面是详细的代码。这里我们先搭建一个简单的转账场景:有一个数据表account,里面有两个用户jack和Rose,用户Jack给用户Rose转账。案例的实现主要使用了mysql数据库、JDBC和C3P0框架。下面是介绍交易案例的详细代码。传输涉及两个DML操作:一个传出和一个传入。这些操作需要是原子的和不可分割的。否则可能会出现数据修改异常。publicclassAccountService{publicbooleantransfer(StringoutUser,StringisUser,intmoney){AccountDaoad=newAccountDao();try{//转移ad.out(outUser,money);//模拟转账过程中异常inti=1/0;//转移到ad.in(inUser,money);}catch(Exceptione){e.printStackTrace();返回假;}返回真;转入和转出是原子的,要么成功要么失败。使用JDBC中事务操作的API启动事务时的注意事项为了保证所有操作都在一个事务中,案例中使用的连接必须相同;service层开启事务的连接需要和dao层访问数据库的连接保持一致。线程并发接下来每个线程只能操作自己的连接,也就是线程隔离。传统的解决方案是基于上面给出的前提。大家通常想到的解决办法就是把connection对象从service层传给dao层。改善了传统方案的缺点。代码的耦合度(因为我们需要从服务层传入连接参数)降低了程序的性能(增加了同步代码块,失去了并发)。这时可以将ThreadLocal绑定到当前线程,减少代码间的差距。耦合使用ThreadLocal来解决上述情况。我们需要改变原来的JDBC连接池对象。改变原来从连接池中获取的对象,直接获取当前线程绑定的连接对象。如果连接对象为空,则去从连接池中获取一个连接,将这个连接对象绑定到当前线程ThreadLocaltl=newThreadLocal();publicstaticConnectiongetConnection(){Connectionconn=tl.get();如果(conn==null){conn=ds.getConnection();tl.set(conn);}returnconn;}ThreadLocal实现的好处从上面的案例我们可以看出,在一些特定的场景下,ThreadLocal方案有两个突出的优势:数据:保存每个线程绑定的数据,在需要的地方直接获取,避免了代码直接传参带来的耦合问题由于性能的损失,ThreadLocal的内部结构通过上面的学习,我们对ThreadLocal的作用有了一定的了解。下面我们来看看ThreadLocal的内部结构,探究一下它是如何实现线程数据隔离的。常见误区如果不看源码,我们可能会猜测ThreadLocal是这样设计的:每个ThreadLocal创建一个Map,然后将线程作为Map的key,将要存储的局部变量作为Map的值,从而达到隔离各个线程局部变量的效果。这是最简单的设计方法。JDK最早的ThreadLocal确实是这样设计的,现在已经不是这样了。不过目前的设计优化了JDK背后的设计方案。JDK8中ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value是真正要存储的值对象。具体过程如下:每个Thread线程在Map内部都有一个Map(ThreadLocalMap),里面存放着ThreadLocal对象键和线程的变量拷贝值。多变的。对于不同的线程,每次获取副本值,其他线程无法获取当前线程的副本值,形成了副本的隔离,互不干扰。从上面走JDK8的设计有什么好处呢?每个Map存储的Entry个数减少了,因为原来Entry的个数由Thread决定,现在由ThreadLocal决定。在实际开发中,Threads的数量远大于ThreadLocals的数量。当Thread被销毁时,ThreadLocalMap也会被销毁,因为ThreadLocal保存在Thread中,随着Thread销毁而消失,可以减少开销。ThreadLocalMap源码分析在分析ThreadLocal方法的时候,我们了解到ThreadLocal的运行实际上是围绕着ThreadLocalMap进行的。ThreadLocalMap的源码比较复杂,我们从以下三个方面来讨论。基本结构ThreadLocalMap是ThreadLocal的一个内部类。它不实现Map接口。它独立实现了Map的功能,其内部的Entry也是独立实现的。成员变量/***初始容量——必须是2**的整数次幂/privatestaticfinalintINITIAL_CAPACITY=16;/***存储数据的表,下面分析Entry类的定义,同样,长度数组的必须是2的整数次方**/privateEntry[]table;/***数组中的entry个数可以判断当前table的使用量是否超过阈值**/privateintsize=0;/***扩容的阈值,当表的使用率大于它时,扩容容量**/privateintthreshold;//默认为0类似于HashMap,INITIAL_CAPACITY代表这个Map的初始容量;table是一个Entry类型的数组,用于存储数据;size表示表中存储的个数;threshold表示需要扩容时对应的大小阈值。存储结构-Entry/**Entry继承WeakRefefence,使用ThreadLocal作为key。如果key是nu11(entry.get()==nu11),说明这个key已经不再被引用,*所以这个时候clear也可以从表中取出entry。*/staticclassEntryextendsweakReference>{objectvalue;条目(ThreadLocal>k,对象v){super(k);value=v;}}在ThreadLocalMap中,Entry也是用来保存K-V结构化数据的。但是Entry中的key只能是ThreadLocal对象,在构造方法中已经做了限制。另外,Entry继承了WeakReference,即key(ThreadLocal)是弱引用,其目的是解除ThreadLocal对象生命周期与线程生命周期的绑定。弱引用和内存泄漏有些程序员会发现在使用ThreadLocal的过程中存在内存泄漏,猜测这个内存泄漏与Entry中使用弱引用的key有关。这种理解其实是错误的。我们先回顾一下这道题涉及的几个名词概念,然后分析题。Memoryleak相关概念Memoryoverflow:内存溢出,没有足够的内存供申请人使用。内存泄漏:内存泄漏是指程序中已经动态分配的堆内存没有被释放或由于某种原因无法释放,造成系统内存的浪费,导致程序运行速度变慢等严重后果程序甚至系统崩溃。I内存泄漏的积累最终会导致内存溢出。Java中有四种类型的引用:强引用、软引用、弱引用和虚引用。目前的问题主要涉及强引用和弱引用:强引用就是我们最常见的普通对象引用。只要有强引用指向一个对象,就可以表明这个对象还“活着”,垃圾回收器不会回收它。一种对象。弱引用:垃圾回收器一旦发现只有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。如果key使用了强引用,会不会有内存泄露?假设ThreadLocalMap中的key使用了强引用,会不会出现内存泄露?此时ThreadLocal的内存映射(实线表示强引用)如下:假设在业务代码中使用了ThreadLocal,回收了threadLocalRef,但是threadLocal无法回收,因为ThreadLocalMap的Entry强引用了threadLocal。在不手动删除Entry且CurrentThread还在运行的前提下,一直存在强引用链threadRef->currentThread->threadLocalMap->entry,Entry不会被回收(ThreadLocal实例和值都包含在内)在Entry中),导致entry内存泄漏。也就是说,ThreadLocalMap中的key使用了强引用,并不能完全避免内存泄漏。如果key使用了弱引用,会不会有内存泄露?同样假设在业务代码中使用ThreadLocal后,threadLocalRef被回收。由于ThreadLocalMap只持有ThreadLocal的弱引用,并没有指向threadlocal实例的强引用,所以threadlocal可以顺利的被gc回收。此时Entry中key=null。但是在不手动删除Entry且CurrentThread还在运行的前提下,还有强引用链threadRef->currentThread->threadLocalMap->entry->value,value不会被回收,这个value会永远不会被访问到,导致值内存泄漏。也就是说ThreadLocalMap中的key使用的是弱引用,同样有可能造成内存泄露。内存泄漏的真正原因对比以上两种情况,我们会发现内存泄漏的发生与ThreadLocalMap中的key是否使用弱引用无关。那么内存泄漏的真正原因是什么?细心的同学会发现,上面两种内存泄漏的情况,有两个前提条件:EntryCurrentThread还在运行,没有手动删除。第一点很好理解,只要使用了ThreadLocal,调用它的remove方法删除对应的Entry,就可以避免内存泄露。第二点稍微复杂一点。由于ThreadLocalMap是Thread的一个属性,被当前线程引用,所以它的生命周期和Thread一样长。那么在使用ThreadLocal之后,如果当前Thread也结束了,那么ThreadLocalMap自然会被gc回收,从根本上避免了内存泄漏。总结一下,ThreadLocal内存泄露的根本原因是:由于ThreadLocalMap的生命周期和Thread一样长,如果不手动删除对应的key,就会导致内存泄露。为什么要使用弱引用?根据刚才的分析,我们知道无论ThreadLocalMap中的key使用哪种类型的引用,都无法完全避免内存泄漏,与使用弱引用无关。有两种方法可以避免内存泄漏:使用ThreadLocal后,调用它的remove方法删除对应的Entry。使用线程池时,线程结束后不会销毁,而是放入线程池中。也就是说,只要记得在使用ThreadLocal后及时调用remove,key是强引用还是弱引用都没有问题。那么为什么键要使用弱引用呢?其实在ThreadLocalMap中的set/getEntry方法中,会判断key为null(即ThreadLocal为null),如果为null,则将value设置为null。这意味着在使用ThreadLocal后,CurrentThread还在运行,即使忘记调用remove方法,弱引用也比强引用多了一层保护:弱引用的ThreadLocal会被回收,并且相应的值会在下一个ThreadLocalMap调用中调用set,get,remove中的任何方法都会被清空,避免内存泄露。