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

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

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

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

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

1. 在进行对象跨层次传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的束缚。

2. 线程间层次隔离。

3. 进行事务操作,用于存储线程事务信息。

4. 数据库连接,Session会话管理。

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

ThreadLocal怎么使用

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

image.png

图片2.png

注:为什么说,ThreadLocal用于局部变量是笨蛋呢?继续看后面的文章会让你大彻大悟的。

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

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

图片3.png

从这张图中可以看到:每一个线程对应了一个ThreadLocalMap,每一个ThreadLocal又包含一个Entry数组。数组的key是我们的ThreadLocal对象,并且是多个线程共享的key,value则是每个线程中各是各的独一份。

ThreadLocal的使用场景—数据库连接

我们知道,数据库连接池最为我们诟病的就是连接的创建与关闭。这其中要耗费大量的资源与时间。我们的ThreadLocal也可以帮我们解决这个问题。

图片4.png

这是一个数据库连接的管理类。我们在使用数据库的时候首先就是建立数据库连接。然后用完了之后就关闭。这样做有一个很严重的问题,如果有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。

 

新建一个ThreadLocal对象:

public final static ThreadLocal<String> resourse = new ThreadLocal<String>(); resourse仅代表一个能够存放String类型的ThreadLocal对象。此时不论什么线程并发访问这个变量,对它进行写入,读取操作,都是线程安全的。

由源码一步一步画出经典流程图

我们根据ThreadLocal在实际开发中的使用流程,把网上到处传遍的经典流程图一步步画出来。(认真看,百分百看懂吊打面试官!)

图片5.png

实际上,画出这个图,只需要三行代码即可。注意:ThreadLocal设置为局部方法仅仅为了写例子。ThreadLocal如果设置为了局部变量将失去他本身将线程隔离的特性作用。完全就是核弹打蚂蚁的操作!

图片6.png图片7.png

首先,如第1步,我们new出一个ThreadLocal对象。

图片8.png

我们知道,ThreadLocal如果不进行set,是没有任何数据的,于是我们进行步骤2开始set一个值。点进set看源码!

图片9.png

点进ThreadLocal的set方法,我们发现它第一步就获取了当前线程的对象。注意,这个当前线程的对象的生命周期是与当前线程同步的。于是更新流程图:

图片10.png

然后我们根据当前线程对象,获取了ThreadLocalMap(这个ThreadLocalMap并不是一直存在的,而是检测我们当前现成是否存在这个ThreadLocalMap对象,如果不存在会先进行对象创建,否则直接获取ThreadLocalMap对象)。于是更新流程:

图片11.png

在获取map对象后,我们开始对当前线程的ThreadLocalMap对象进行set操作。

图片12.png

注意,此处的set的key是this。此时的this对象正是我们的ThreadLocal的对象,如图所示:

图片13.png

那么这个ThreadLocalMap对象的set方法又干了些什么呢?我们继续进去看。

image.png

我们可以看到。我们把数据重新处理,放入了一个Entry数组中。那么这个Entry数组又是什么呢?

我们来看一下Entry类的结构:

image.png

可以看到Entry内部结构像一个key-value键值对的Map一样,因此更新一下流程:

图片14.png

然后我再来观察Entry的数据类型:

image.png

最最最重点的来了。就是这个Entry的key这里是弱引用,该引用指向ThreadLocal的对象。what?弱引用?这是干什么用的?不要急,保持你的疑惑。我们先跟着上述步骤更新我们的流程图:

图片16.png

图片17.png

终于,到了这一步,和我们最经典的图相吻合了。这时候我们长出一口气,总算完啦!不!我说没完。还有最最关键的一步。

我们知道弱引用的特性是在一次GC后,与对象之间的联系断开。那么程序在运行一段时间,随便发生一次GC后,整个内存图是这样的。这才最后内存中数据的分布!

图片18.png

那有人又说?好家伙,你图都成这样了,我再通过ref.get()方法获取值还能获取到吗!稍安勿躁,这就带你继续看。

image.png

我们发现,诶当我们去get当前线程的ThreadLocal数据时,我们也是获取当前线程,再次委托给我们的ThreadLocalMap去查询。当执行到getEntry时,我们传入参数this,正是我们的当前对象的引用ref。

因此,程序就变为了去获取当前线程的TreadLocalMap的Entry的key为ref的value数据。也就是走的图中的2号路线。

image.png

有没有毛瑟顿开的感觉!这些总算可以收工了吧?当你准备长出一口气时,我说还没有!因为博主一开始就有一个疑惑。就是我Entry的key执行ref对象的引用断开时,我Entry中的key不会变为null么?答案我们继续揭晓。

弱引用解读

我们知道java中有强软弱虚4种引用,而弱引用的定义就是只要发生gc,那么引用链就会断开。我们来用以程序测试一下弱引用。

首先,我们先随意定义一个类测试类。

图片21.png

其次,我们使用弱引用引用这个类。我们测试以下程序在发生一次GC后,wrTest的结果是否为null。

图片22.png

图片23.png

此时我们看到,该对象的确已经为null了。此时,我们更换写法。

图片24.png

图片26.png

诶?问题来了。为什么这个弱引用在发生一次GC后,值依然可以获取到呢?是弱引用的引用链没有消失么?不,真相是我们此时的new Test()对象也恰巧被一个test强引用所指向,因此发生了GC也无法回收掉。这与我们ThreadLocal中,Entry的key断开与new ThreadLocal()的引用链,却依旧不为null的场景完全吻合。

图片27.png

我们得到结论:即使弱引用所指向的对象与弱引用断开引用链,但若是该对象有其他地方引用而导致无法回收,那么我的弱引用依旧可以通过断开前的连接地址去获取值。(也就是说引用的断开不会影响我们引用的寻址功能。引用的断开只会导致引用链断开导致对象被GC回收,但是!此时若有一个强引用引用着,那么弱引用就可以在无引用链的情况下继续访问该对象。(这里扩展一下。若对象的地址强制改变,弱引用将无法继续跟踪)。

举一个简单的案例:假设你买票上火车,找到了座位做了进去。但是记性很差的你,上了个厕所回来找不到自己的座位了。此时,列车员始终可以根据你的购票档案查到你的座位号。

到此为止,ThreadLocal的源码图解可以告一段落了。

为什么ThreadLocalMap中的key要设置成弱引用?

ThreadLocal的被回收的场景

图片28.png

首先,强调一下这个假设的前提是ThreadLocal的用法使用不到位导致的,不优雅的。为什么博主这么说呢?因为ThreadLocal为了可以拥有在每个线程直接独立创建副本的能力,我们通常会把它用public static final进行修饰。也就是说这个引用不出意外将永远不会消失。

有人会反驳说,虽然你这个引用用public static final进行修饰不会消失,但是线程会执行结束啊?如果仔细读了上述流程的读者应该已经很明确我们ThreadLocal获取值是根据当前线程的ThreadLocalMap获取的,如果当前线程结束,那么该线程的ThreadLocalMap对象会一起消失。对应的Entry也会一起消失。(后续还有讲解)

内存泄漏的原因

图片29.png

我们之前在讲解流程的时候,讲过ThreadMap中的Entry是弱引用。图片30.png

那么此时,我们逆向思考ThreadLocalMap中Entry的key是强引用,那么当我们的ref出栈后,1号线断开后,Entry就会始终有一个2号引用指向new ThreadLocal()对象,导致该对象永远无法访问,也无法回收,导致内存泄漏。

图片31.png

为了避免这种尴尬,Entry的key与new ThreadLocal的对象设置为弱引用。(咱哥俩联系一次就得了,以后找你讨债没问题,你是死是活我管不着)。着实把该对象当成了工具人!

设置为弱引用后,经过一次GC内存模型如下:

图片32.png

此时,当ref出栈,new ThreadLoal孤立无援,唯有被回收的下场。到此,最常见的内存泄漏讲解完毕。

很多网上的博客,都是这么解析的。虽然光论结果来说都能说通,但是其实是本质对ThreadLocal并没有深刻的理解。

image.png

当步骤1断开后,步骤2再次经过垃圾回收断开,对象才被孤立无援被回收。此处我很自信的说:2其实在1断开之前就和对象彻底决裂分手再无瓜葛了!如果还没理解,就继续把我上述分析流程再看看。

Entry的key内存泄漏

我们之前看的博客说的最多的就是ThreadLocal对象的内存泄漏。然而其实我们发现Entry其实也有泄漏。如图,由于我们将ThreadLocal对象的成功回收,这些我们的key”终于”变为null了。但是我们的value依旧存在,因此这一组数据的value由于key为null的原因也无法访问导致内存泄漏。

呀!这可咋办,之前看的博客没人提过啊!别急,我们来看ThreadLocal是如何应对的。

图片34.png

 

Set优化

图片35.png

此时,当Entry的下标i对应的key值为null的话,说明key已经被回收了,那么直接把位置继续占用即可,反正key为null已经没用了。

Get优化

图片36.png

 

图片37.png

图片38.png

 

图片39.png

可以看到,get发现key为null的处理方式是直接从Entry中强行删除。

Remove方法

图片40.png

remove是我们主动触发,清理Entry的方式。和get方法底层调用的是同一个方法。可以加速我们泄漏的内存回收。因此,如果当栈中的引用变为null时,我们可以再次调用remove()方法,将ThreadLocalMap中的Entry进行清理。(更具时效性)

线程退出时优化

最后,当线程退出的时候,Thread类会进行清理操作。其中就包括清理ThreadLocalMap。

线程退出执行的exit()方法。

图片42.png

图片43.png图片44.png

ThradLocal可以设置成局部变量,可以但没意义,而且有内存泄漏风险

内存泄漏讲了这么这么多!其实我们发现导致内存泄漏的原因就是这个ThreadLocal设置成了局部变量,导致ThreadLocal对象在线程结束前被回收。此时就会造成内存泄漏一直到线程结束才可以释放掉的风险。如果一定要这么写,那么一定记得在ThreadLocal对象回收时调用一下remove()方法及时释放内存。

另外,threadLocal如果设置成局部变量,那么同一个线程中的其他方法也无法获取当该对象。这样也就背离了ThreadLocal在同一个线程下,共享同一个变量的设计初衷了。核弹杀蚂蚁。

ThreadLocal的错误使用导致线程好像不安全了?

图片46.png图片47.png

这个代码原本的意图是,每个线程的ThreadLocal对象分别执行一次数据的累加,理论上每个线程打印的值应该都是2。但结果却是5。

当ThreadLocal操作相同对象的时候,所有的操作都指向同一个实例。如果想让上面的程序正常运行,需要每一个ThreadLocal都持有一个新的实例。

所以这个线程不安全完全是因为使用者错误的使用,ThreadLocal绝对是线程安全的。

ThreadLocal的正确使用

之前讲的例子,都是基于错误的写法而画出的图:

image.png

对应的流程图是这样:

图片49.png

至于这么写为什么有问题,如果你还有问题,那说明上面的内容没有吃透,建议返回仔细再看。

正确的用法:

image.png

设置为全局变量,让所有线程共享方法,那么图就变为了:

image.png

 

 

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大将黄猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值