说ThreadLocal

文章介绍了ThreadLocal的作用、使用场景,强调了它与Synchronized的区别,以及如何避免内存泄露。讨论了ThreadLocalMap的结构和清理机制,以及正确使用ThreadLocal的实践方法。
摘要由CSDN通过智能技术生成

ThreadLocal叫做线程变量,与普通变量的区别在于:每个使用该变量的线程都会初始化一个完全独立的实例副本。

适用于以下两种场景:

  1. 每个线程需要有自己单独的实例

  2. 实例需要在多个方法中共享,但不希望被多线程共享

对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。

对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。

场景:

  1. 存储用户Session

  2. 数据库连接,处理数据库事务

  3. 数据跨层传递(controller,service, dao)

  4. Spring使用ThreadLocal解决线程安全问题

在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦

ThreadLocal与Synchronized

ThreadLocal和Synchronized都用于解决多线程并发访问。

但ThreadLocal与Synchronized有本质的区别:

  1. Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离

  2. Synchronized是利用锁的机制,使变量或代码块在某一时刻只能被一个线程访问。

  3. ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

  4. Synchronized用于多个线程间通信时能够获得数据共享

threadlocal是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry,虽然不同的线程之间threadlocal这个key值是一样的,但不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。

ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。

Java的四种引用类型

  • 强引用:我们通常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不够时

  • 软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收

  • 弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收

  • 虚引用:虚引用是最弱的引用,在Java中使用PhantomReference进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知

ThreadLocal与Thread,ThreadLocalMap之间的关系

  1. 每一个Thread线程内部都有一个Map(ThreadLocalMap)

  2. Map里面存储ThreadLocal对象(key)和线程的变量副本(value)

  3. Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值

  4. 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰

ThreadLocalMap其实是Thread线程的一个属性值,而ThreadLocal是维护ThreadLocalMap,这个属性是指一个工具类,Thread线程可以拥有多个ThreadLocal维护自己线程独享的共享变量(这个共享变量只是针对自己线程里面共享)

ThreadLocal使用原理

当线程使用ThreadLocal时,是将threadlocal当做当前线程thread的属性ThreadLocalMap中的一个Entry的key值,实际上存放的变量是Entry的value值,我们实际要使用的值是value值。

ThreadLocal的remove方法

remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。

ThreadLocal 内存泄露的原因

Entry将ThreadLocal作为Key,值作为value保存,它继承自WeakReference,这意味着ThreadLocal对象是一个「弱引用」。

主要两个原因 1 . 没有手动删除这个 Entry 2 . CurrentThread 当前线程依然运行

第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法删除对应的 Entry ,就能避免内存泄漏。 第二点稍微复杂一点,由于ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟 Thread 一样长。如果threadlocal变量被回收,那么当前线程的threadlocal 变量副本指向的就是key=null, 也即entry(null,value),那这个entry对应的value永远无法访问到。实际私用ThreadLocal场景都是采用线程池,而线程池中的线程都是复用的,这样就可能导致非常多的entry(null,value)出现,从而导致内存泄露。 综上, ThreadLocal 内存泄漏的根源是: 由于ThreadLocalMap 的生命周期跟 Thread 一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏.

为什么不将key设置为强引用

为什么ThreadLocalMap的key要设计成弱引用呢?其实很简单,如果key设计成强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期。

    假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了,但是因为threadLocalMap的Entry强引用了threadLocal(key就是threadLocal), 造成ThreadLocal无法被回收。在没有手动删除Entry以及CurrentThread(当前线程)依然运行的前提下, 始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry, Entry就不会被回收( Entry中包括了ThreadLocal实例和value), 导致Entry内存泄漏也就是说: ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。

为什么 key 要用弱引用

在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的.这就意味着使用threadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏.

如何正确的使用ThreadLocal

  1. 将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露

  2. 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

ThreadLocalMap的两种过期key数据清理方式:探测式清理启发式清理

探测式清理,也就是expungeStaleEntry方法,遍历散列数组,从开始位置向后探测清理过期数据,将过期数据的Entry设置为null,沿途中碰到未过期的数据则将此数据rehash后重新在table数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的Entry=null的桶中,使rehash后的Entry数据距离正确的桶的位置更近一些

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值