ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与Synchronized有着本质的区别。Synchronized是利用锁的机制,使变量或代码代码块在某一个时刻仅仅能被一个线程访问。java
从名字咱们就能够看到ThreadLocal叫作线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其余线程而言是隔离的。ThreadLocal为变量在每一个线程中都建立了一个副本,那么每一个线程能够访问本身内部的副本变量。面试
从字面意思很是容易理解,可是从实际使用的角度来看就没那么容易了。做为一个面试常问的点,使用场景那也是至关的丰富。数据库
如今应该对ThreadLocal已经有一个大概的认识了。下面看看具体如何使用。数组
既然ThreadLocal的做用是每个线程建立一个副本,那咱们使用一个例子来验证一下:安全
从结果可知,每个线程都有各自的local值。也就是说,threadLocal的值是线程与线程分离的。具体原理能够画出如下不一样线程中ThreadLocalMap是如何存储数据的。服务器
若是是第一次学习ThreadLocal的朋友可能看懵了,ThreadLocal我都没看懂,你跟我说ThreadLocalMap?别急,咱们接着往下看。markdown
这里整理了最近BAT最新面试题,2021船新版本!!须要的朋友能够点击:这个,点这个!!,备注:jj。但愿那些有须要朋友能在今年第一波招聘潮找到一个本身满意顺心的工做!
多线程
咱们知道,数据库链接池最为咱们诟病的就是链接的建立与关闭。这其中要耗费大量的资源与时间。咱们的ThreadLocal也能够帮咱们解决这个问题。并发
这是一个数据库链接的管理类。咱们在使用数据库的时候首先就是创建数据库链接。而后用完了以后就关闭。这样作有一个很严重的问题,若是有1个用户频繁使用数据库,那么就须要创建屡次链接和关闭。这样咱们服务器可能吃不消,那么怎么办呢?若是一万个客户端,那么服务器压力更大。工具
这时最好使用ThreadLocal。由于ThreadLocal在每一个线程中会建立一个副本。而且在线程内部任何地方可使用。线程之间互不影响。这样一来就不存在线程安全问题,也不会严重影响程序执行性能,避免了connection的频繁建立和销毁。(固然实际中咱们有数据库链接池能够处理,但咱们的目的都很明确,避免链接对象的频繁建立与销毁!)
以上主要讲解了一个基本的案例,而后还分析了为何在数据库链接的时候会使用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的源码图解能够告一段落了。
首先,强调一下这个假设的前提是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断开以前就和对象完全决裂分手再无瓜葛了!若是还没理解,就继续把我上述分析流程再看看。
咱们以前看的博客说的最多的就是ThreadLocal对象的内存泄漏。然而其实咱们发现Entry其实也有泄漏。如图,因为咱们将ThreadLocal对象的成功回收,这些咱们的key”终于”变为null了。可是咱们的value依旧存在,所以这一组数据的value因为key为null的缘由也没法访问致使内存泄漏。
呀!这可咋办,以前看的博客没人提过啊!别急,咱们来看ThreadLocal是如何应对的。
此时,当Entry的下标i对应的key值为null的话,说明key已经被回收了,那么直接把位置继续占用便可,反正key为null已经没用了。
能够看到,get发现key为null的处理方式是直接从Entry中强行删除。
remove是咱们主动触发,清理Entry的方式。和get方法底层调用的是同一个方法。能够加速咱们泄漏的内存回收。所以,若是当栈中的引用变为null时,咱们能够再次调用remove()方法,将ThreadLocalMap中的Entry进行清理。(更具时效性)
最后,当线程退出的时候,Thread类会进行清理操做。其中就包括清理ThreadLocalMap。
线程退出执行的exit()方法。
内存泄漏讲了这么这么多!其实咱们发现致使内存泄漏的缘由就是这个ThreadLocal设置成了局部变量,致使ThreadLocal对象在线程结束前被回收。此时就会形成内存泄漏一直到线程结束才能够释放掉的风险。若是必定要这么写,那么必定记得在ThreadLocal对象回收时调用一下remove()方法及时释放内存。
另外,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这个重点知识,帮助概括吸取更多解决问题的思路。吊打面试官和那条”该死”的弱引用同样,只是顺手搞定的事儿了。