还不理解ThreadLocal?那是你还没读到这篇文章

与Synchonized的比较,它的作用是什么

ThreadLocal和Synchonized都用于解决多线程并发访问。可是ThreadLocal与Synchronized有着本质的区别。Synchronized是利用锁的机制,使变量或代码代码块在某一个时刻仅仅能被一个线程访问。

从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

从字面意思非常容易理解,但是从实际使用的角度来看就没那么容易了。作为一个面试常问的点,使用场景那也是相当的丰富。

  1. 在进行对象跨层次传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的束缚。
  2. 线程间层次隔离。
  3. 进行事务操作,用于存储线程事务信息。
  4. 数据库连接,Session会话管理。

现在应该对ThreadLocal已经有一个大概的认识了。下面看看具体如何使用。

ThreadLocal怎么使用

既然ThreadLocal的作用是每一个线程创建一个副本,那我们使用一个例子来验证一下:

 

 

扫描二维码关注公众号,回复: 12738829 查看本文章

从结果可知,每一个线程都有各自的local值。也就是说,threadLocal的值是线程与线程分离的。具体原理可以画出以下不同线程中ThreadLocalMap是如何存储数据的。

如果是第一次学习ThreadLocal的朋友可能看懵了,ThreadLocal我都没看懂,你跟我说ThreadLocalMap?别急,我们接着往下看。

 

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<String> resourse = new ThreadLocal<String>(); 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都持有一个新的实例。

猜你喜欢

转载自blog.csdn.net/weixin_47184173/article/details/111992933