线程局部变量(ThreadLocal)原理分析

一、ThreadLocal的作用

1.1 来源

    万物皆有源,事物的存在不是偶然的,它们都有其自身的根源。ThreadLocal作为Java开发者日常使用的一个工具,它的起源是什么呢?ThreadLocal的出现,主要是作为解决Java并发问题的一种手段,通过存储线程的局部变量,从而实现线程间资源的隔离,避免因多线程访问共享变量导致的并发问题。所以可以看出,ThreadLocal解决并发问题使用的手段是:避免共享

1.2 简单应用

    那么如何使用ThreadLocal呢?举一个比较简单的例子,比如有一个基于Tomcat的Web服务器,每个请求都由一个单独的线程来执行,那么可以通过一个拦截器,在每次请求进来时,将本次请求的用户名、session等信息保存在ThreadLocal中,那么在任意一处逻辑代码中都可以从ThreadLocal中取出这些信息来使用。

二、ThreadLocal的实现原理

2.1 ThreadLocal实现原理猜想

    在研究ThreadLocal源码之前,可以简单地猜想其底层的实现(其实在阅读一些源码之前,都可以先根据功能对其底层实现进行猜测,然后再通过看源码来验证自己的猜想)。既然ThreadLocal的作用是让每个线程拥有不同的变量,那么可以在ThreadLocal内部保存一个Map,key为线程,value则为变量,就可以做到每个线程拥有各自的值了。如图所示:

 

2.2 猜想验证

    查看JDK1.8版本的ThreadLocal类,发现其内部确实有一个Map,名称为ThreadLocalMap(但是它是一个内部类),但是持有该类对象的是Thread类(在Thread类内部定义了这个属性:ThreadLocal.ThreadLocalMap threadlocal = null),而不是ThreadLocal类。所以实际上的实现与上述的猜想不符,实际上的结构如图:

    分析这种实现方式,发现ThreadLocal只是一个代理工具类,其内部不持有任何与线程相关的数据,与线程相关的数据都是保存在Thread内部。从数据的亲缘性上来讲,ThreadLocalMap确实也应该属于Thread会更加合理。当然更加重要的原因是2.1中的实现方案会造成内存泄漏,因为ThreadLocal中的Map对Thread进行引用,那么只要ThreadLocal对象存在(往往贯穿整个应用的生命周期),Thread对象就无法被回收,从而造成内存泄漏。

    ThreadLocalMap类中维护ThreadLocal到Value的映射不是简单地采用Map,而是使用了Entry,并且对ThreadLocal还是弱引用(可对弱引用做这样简单理解:发生GC时,无论内存是否足够,只被弱引用的对象都会被回收),如下:

    那么为什么使用弱引用?或者说为什么不使用强引用呢?如果使用强引用,当我们不需要某个ThreadLocal对象后,将其引用置为null(即不再持有该对象的引用,使其不可达,以便在下次GC的时候将其回收),但这时ThreadLocalMap对象还对该ThreadLocal对象持有强引用,那么该ThreadLocal对象就无法被回收。如果使用了线程池,线程中的ThreadLocalMap所持有的ThreadLocal就会越来越多,导致内存泄漏。

    虽然Java的实现在尽力避免内存泄漏,但是它实际上还是存在内存泄漏的可能。

三、ThreadLocal内存泄漏

3.1 内存泄漏的原因    

    ThreadLocal发生内存泄漏的地方也是ThreadLocalMap,可以从上文的代码看到,ThreadLocalMap中对ThreadLocal是弱引用,对value是强引用,当ThreadLocal对象被回收后,虽然对应对应Entry对象的key所引用的对象以被回收,但是它还持有value对象的强引用,导致value无法释放,value一直增加也会造成内存泄漏。

    那么有人会说,既然对value强引用会造成value无法回收,从而引起内存泄漏,为什么不干脆设计成和ThreadLocal一样,使用弱引用呢?上文提到,如果一个对象只被弱引用,在GC的时候就会被回收,那么就会出现这么一种情况,明明往ThreadLocal中设置了值,取出来的时候却是null,这就是因为GC的时候把value对象回收掉导致的。所以,ThreadLocalMap对value如果是弱引用的话,会导致从ThreadLocal中拿到的值发生错乱。

    那么又有人会问了,既然GC的时候value会因为弱引用被回收,为什么ThreadLocal不会被回收呢?实际上ThreadLocal对象也可能被回收,但是JDK官方建议使用ThreadLocal时将其定义为静态变量,对ThreadLocal对象始终持有强引用,使其在GC时不会被回收。

3.2 如何应对内存泄漏

    每次使用完ThreadLocal都调用其remove()方法,其中会对Entry中的该ThreadLocal对象及其对应的value进行清除:

    当然,JDK的开发者不会将所有希望寄托于使用者在每次使用完ThreadLocal后都调用remove()方法来避免内存泄漏,他们在很多地方都埋下一个逻辑:清理key=null的Entry中对应的value。比如最常用的ThreadLocal.set()方法,会查找出下一个key=null的Entry,然后对其value进行清理,源码如下:

    另外,在ThreadLocal的get()、remove()方法中也存在相似的逻辑。当然,在最极端的情况下,不调用get()、set()、remove()方法,那么就无法清除那些key=null的Entry中的value了。

四、正确使用ThreadLocal

正确用法:    

1.为了避免弱引用导致GC时ThreadLocal对象被回收,官方建议将ThreadLocal定义为静态变量,这样就会对该ThreadLocal对象一直持有一个强引用,使其不会在GC时被意外回收,从而导致ThreadLocal数据丢失。

2.使用try-finally的方式来使用ThreadLocal,在finally中调用ThreadLocal对象的remove()方法,清理对应的数据。

五、扩展

    Spring中bean都是默认单例的,在并发的情况下对共享变量的操作会存在线程安全问题,Spring为了解决这个问题,就使用了ThreadLocal。

    要想实现JDBC事务, 就必须是在同一个连接对象中操作(多个连接下事务就会不可控, 需要借助分布式事务完成)。那Spring 如何保证数据库事务在同一个连接下执行的呢?DataSourceTransactionManager 是Spring的数据源事务管理器, 它会在你调用getConnection()的时候从数据库连接池中获取一个connection, 然后将其与ThreadLocal绑定, 事务完成后解除绑定。这样就保证了事务在同一连接下完成。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值