45张图搞定!ThreadLocal的最牛辨析!
与Synchonized的比较,它的作用是什么
ThreadLocal和Synchonized都用于解决多线程并发访问。可是ThreadLocal与Synchronized有着本质的区别。Synchronized是利用锁的机制,使变量或代码代码块在某一个时刻仅仅能被一个线程访问。
从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
从字面意思非常容易理解,但是从实际使用的角度来看就没那么容易了。作为一个面试常问的点,使用场景那也是相当的丰富。
- 在进行对象跨层次传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的束缚。
- 线程间层次隔离。
- 进行事务操作,用于存储线程事务信息。
- 数据库连接,Session会话管理。
现在应该对ThreadLocal已经有一个大概的认识了。下面看看具体如何使用。
ThreadLocal怎么使用
既然ThreadLocal的作用是每一个线程创建一个副本,那我们使用一个例子来验证一下:
从结果可知,每一个线程都有各自的local值。也就是说,threadLocal的值是线程与线程分离的。具体原理可以画出以下不同线程中ThreadLocalMap是如何存储数据的。
如果是第一次学习ThreadLocal的朋友可能看懵了,ThreadLocal我都没看懂,你跟我说ThreadLocalMap?别急,我们接着往下看。
这里整理了最近BAT最新面试题,2021船新版本!!需要的朋友可以点击:这个,点这个!!,希望那些有需要朋友能在今年第一波招聘潮找到一个自己满意顺心的工作!
ThreadLocal的使用场景—数据库连接
我们知道,数据库连接池最为我们诟病的就是连接的创建与关闭。这其中要耗费大量的资源与时间。我们的ThreadLocal也可以帮我们解决这个问题。
这是一个数据库连接的管理类。我们在使用数据库的时候首先就是建立数据库连接。然后用完了之后就关闭。这样做有一个很严重的问题,如果有1个用户频繁使用数据库,那么就需要建立多次连接和关闭。这样我们服务器可能吃不消,那么怎么办呢?如果一万个客户端,那么服务器压力更大。
这时最好使用ThreadLocal。因为ThreadLocal在每个线程中会创建一个副本。并且在线程内部任何地方可以使用。线程之间互不影响。这样一来就不存在线程安全问题,也不会严重影响程序执行性能,避免了connection的频繁创建和销毁。(当然实际中我们有数据库连接池可以处理,但我们的目的都很明确,避免连接对象的频繁创建与销毁!)
以上主要讲解了一个基本的案例,然后还分析了为什么在数据库连接的时候会使用ThreadLocal。下面我们从源码的角度分析ThreadLocal的工作原理。
ThreadLocal源码分析
ThreadLocal类接口简介
ThreadLocal类接口很简单,只有4个方法,先来了解一下:
1. void set(Object value);//设置当前线程的线程局部变量值
2. public Object get();//该方法返回当前线程所对应的线程局部变量
3. public void remove();//当线程局部变量的值删除,目的是为了减少内存的占用。该方法是JDK5.0新增的方法,需要指出的是,当前线程结束后,对应该线程的局部变量将自动被垃圾回收,所以调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
4. protected Object initialValue();//返回该线程局部变量的初始值,该方法是一个protected方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。如果不写initialValue,那么第一次调用get()会返回一个null。
5. public final static ThreadLocal resourse = new ThreadLocal();//resourse仅代表一个能够存放String类型的ThreadLocal对象。此时不论什么线程并发访问这个变量,对它进行写入,读取操作,都是线程安全的。
由源码一步一步画出经典流程图
我们根据ThreadLocal在实际开发中的使用流程,把网上到处传遍的经典流程图一步步画出来。(认真看,百分百看懂吊打面试官!)
实际上,画出这个图,只需要三行代码即可。注意:ThreadLocal设置为局部方法仅仅为了写例子。ThreadLocal如果设置为了局部变量将失去他本身将线程隔离的特性作用。完全就是核弹打蚂蚁的操作!
首先,如第1步,我们new出一个ThreadLocal对象。
我们知道,ThreadLocal如果不进行set,是没有任何数据的,于是我们进行步骤2开始set一个值。点进set看源码!
点进ThreadLocal的set方法,我们发现它第一步就获取了当前线程的对象。注意,这个当前线程的对象的生命周期是与当前线程同步的。于是更新流程图:
然后我们根据当前线程对象,获取了ThreadLocalMap(这个ThreadLocalMap并不是一直存在的,而是检测我们当前现成是否存在这个ThreadLocalMap对象,如果不存在会先进行对象创建,否则直接获取ThreadLocalMap对象)。于是更新流程:
在获取map对象后,我们开始对当前线程的ThreadLocalMap对象进行set操作。
注意,此处的set的key是this。此时的this对象正是我们的ThreadLocal的对象,如图所示:
那么这个ThreadLocalMap对象的set方法又干了些什么呢?我们继续进去看。
我们可以看到。我们把数据重新处理,放入了一个Entry数组中。那么这个Entry数组又是什么呢?先更新一下流程:
我们来看一下Entry类的结构。
我们可以看到,Entry的结构非常类似一个map,最最最重点的来了。就是这个Entry的key这里是弱引用。what?弱引用?这是干什么用的?不要急,保持你的疑惑。我们先跟着上述步骤更新我们的流程图:
终于,到了这一步,和我们最经典的图相吻合了。这时候我们长出一口气,总算完啦!不!我说没完。还有最最关键的一步。
我们知道弱引用的特性是在一次GC后,与对象之间的联系断开。那么程序在运行一段时间,随便发生一次GC后,整个内存图是这样的。这才最后内存中数据的分布!
那有人又说?好家伙,你图都成这样了,我再通过ref.get()方法获取值还能获取到吗!稍安勿躁,这就带你继续看。
我们发现,诶当我们去get当前线程的ThreadLocal数据时,我们也是获取当前线程,再次委托给我们的ThreadLocalMap去查询。那么流程是这样的。
我们从步骤1的存在目的,进入当前线程的步骤2,去获取当前线程key为ref的value数据。有没有茅塞顿开的感觉!这些总算可以收工了吧?当你准备长出一口气时,我说还没有!因为博主一开始就有一个疑惑。就是我Entry的key执行ref对象的引用断开时,我Entry中的key不会变为null么?答案我们继续揭晓。
弱引用解读
我们知道java中有强软弱虚4种引用,而弱引用的定义就是只要发生gc,那么引用链就会断开。我们来用程序测试一下弱引用。
首先,我们先随意定义一个类测试类。
其次,我们使用弱引用引用这个类。我们测试以下程序在发生一次GC后,wrTest的结果是否为null。
此时我们看到,该对象的确已经为null了。此时,我们更换写法。
诶?问题来了。为什么这个弱引用在发生一次GC后,值依然可以获取到呢?是弱引用的引用链没有消失么?不,真相是我们此时的new Test()对象也恰巧被一个test强引用所指向,因此发生了GC也无法回收掉。这与我们ThreadLocal中,Entry的key断开与new ThreadLocal()的引用链,却依旧不为null的场景完全吻合。
我们得到结论:即使弱引用所指向的对象与弱引用断开引用链,但若是该对象有其他地方引用而导致无法回收,那么我的弱引用依旧可以通过断开前的连接地址去获取值。(也就是说引用的断开不会影响我们引用的寻址功能。引用的断开只会导致引用链断开导致对象被GC回收,但是!此时若有一个强引用引用着,那么弱引用就可以在无引用链的情况下继续访问该对象。(这里扩展一下。若对象的地址强制改变,弱引用将无法继续跟踪))。
举一个简单的案例:假设你买票上火车,找到了座位坐了进去。但是记性很差的你,上了个厕所回来找不到自己的座位了。此时,列车员始终可以根据你的购票档案查到你的座位号。
到此为止,ThreadLocal的源码图解可以告一段落了。
为什么ThreadLocalMap中的key要设置成弱引用?
ThreadLocal的被回收的场景
首先,强调一下这个假设的前提是ThreadLocal的用法使用不到位导致的,不优雅的。为什么博主这么说呢?因为ThreadLocal为了可以拥有在每个线程直接独立创建副本的能力,我们通常会把它用public static final进行修饰。也就是说这个引用不出意外将永远不会消失。
有人会反驳说,虽然你这个引用用public static final进行修饰不会消失,但是线程会执行结束啊?如果仔细读了上述流程的读者应该已经很明确我们ThreadLocal获取值是根据当前线程的ThreadLocalMap获取的,如果当前线程结束,那么该线程的ThreadLocalMap对象会一起消失。对应的Entry也会一起消失。(后续还有讲解)
内存泄漏的原因
我们之前在讲解流程的时候,讲过ThreadMap中的Entry是弱引用。
那么此时,我们逆向思考ThreadLocalMap中Entry的key是强引用,那么当我们的ref出栈后,1号线断开后,Entry就会始终有一个2号引用指向new ThreadLocal()对象,导致该对象永远无法访问,也无法回收,导致内存泄漏。
为了避免这种尴尬,Entry的key与new ThreadLocal的对象设置为弱引用。(咱哥俩联系一次就得了,以后找你讨债没问题,你是死是活我管不着)。着实把该对象当成了工具人!
设置为弱引用后,经过一次GC内存模型如下:
此时,当ref出栈,new ThreadLoal孤立无援,唯有被回收的下场。到此,最常见的内存泄漏讲解完毕。
很多网上的博客,都是这么解析的。虽然光论结果来说都能说通,但是其实是本质对ThreadLocal并没有深刻的理解。
当步骤1断开后,步骤2再次经过垃圾回收断开,对象才被孤立无援被回收。此处我很自信的说:2其实在1断开之前就和对象彻底决裂分手再无瓜葛了!如果还没理解,就继续把我上述分析流程再看看。
Entry的key内存泄漏
我们之前看的博客说的最多的就是ThreadLocal对象的内存泄漏。然而其实我们发现Entry其实也有泄漏。如图,由于我们将ThreadLocal对象的成功回收,这些我们的key”终于”变为null了。但是我们的value依旧存在,因此这一组数据的value由于key为null的原因也无法访问导致内存泄漏。
呀!这可咋办,之前看的博客没人提过啊!别急,我们来看ThreadLocal是如何应对的。
Set优化
此时,当Entry的下标i对应的key值为null的话,说明key已经被回收了,那么直接把位置继续占用即可,反正key为null已经没用了。
Get优化
可以看到,get发现key为null的处理方式是直接从Entry中强行删除。
Remove方法
remove是我们主动触发,清理Entry的方式。和get方法底层调用的是同一个方法。可以加速我们泄漏的内存回收。因此,如果当栈中的引用变为null时,我们可以再次调用remove()方法,将ThreadLocalMap中的Entry进行清理。(更具时效性)
线程退出时优化
最后,当线程退出的时候,Thread类会进行清理操作。其中就包括清理ThreadLocalMap。
线程退出执行的exit()方法。
ThradLocal可以设置成局部变量,可以但没意义,而且有内存泄漏风险
内存泄漏讲了这么这么多!其实我们发现导致内存泄漏的原因就是这个ThreadLocal设置成了局部变量,导致ThreadLocal对象在线程结束前被回收。此时就会造成内存泄漏一直到线程结束才可以释放掉的风险。如果一定要这么写,那么一定记得在ThreadLocal对象回收时调用一下remove()方法及时释放内存。
另外,threadLocal如果设置成局部变量,那么同一个线程中的其他方法也无法获取当该对象。这样也就背离了ThreadLocal在同一个线程下,共享同一个变量的设计初衷了。核弹杀蚂蚁。
ThreadLocal的错误使用导致线程不安全
由图可见,当ThreadLocal操作相同对象的时候,所有的操作都指向同一个实例。如果想让上面的程序正常运行,需要每一个ThreadLocal都持有一个新的实例。
总结
其实平时我们从书本中获取到ThreadLocal知识足以面对我们应付各种场景的面试了。但是笔者最开始即使大致清楚了ThreadLocal的大致工作流程,却有许多细节没有串起来。本文的目的不仅仅是让各位读者拥有应付面试的能力,更是带着大家比较精细的分析了ThreadLocal的设计思路。我们往往学习一门新的技术时,要站在这个技术出现之前的开发人员面临的问题。ThreadLocal就解决了同一线程中的数据共享问题。
那么我们要解决同一线程间数据的共享问题,我们就需要拿到这个线程所有的方法共享的对象。于是我们开发人员在操作ThreadLocal的绝大部分方法时,第一步永远是获取当前线程对象。再由这个当前线程对象维护一个类似于Map的Entry。以ThreadLocal对象作为key,存放仅仅属于当前线程的value,从而达到线程分离。
我们要完全弄懂ThreadLocal,不能跟随很多博客上讲的,上来直接就硬着头皮开始解决弱引用的问题。我们首先要先把自己幻想成开发人员,一步一步在脑袋中画出ThreadLocal的工作流程。把ThreadLocalMap的Entry的key引用ThreadLocal对象的图像模拟出来(流程如果有还是不太清楚的朋友,可以再仔细看看上文讲解的流程图)。
此时,我们很明确的知道了ThreadLocalMap的Entry的key引用ThreadLocal对象这条引用存在的意义了,但是,如果这条引用设置成强引用就不可避免的导致我们的ThreadLocal对象发生了内存泄漏。于是我们才想到了使用弱引用去解决内存泄漏问题。
同时,通过讲解弱的例子,我们了解到只要被弱引用引用过的对象,即使经过GC导致弱引用链断开,只要该对象仍有强引用引用着让它不被GC,那么弱引用依旧不会为null的小细节。
希望通过这个TL这个重点知识,帮助归纳吸收更多解决问题的思路。吊打面试官和那条”该死”的弱引用一样,只是顺手搞定的事儿了。