ThreadLocal原理,内存泄漏问题,怎么解决

ThreadLocal的作用

ThreadLocal是在线程使用共享资源但共享资源并不用来通信的时候,即不是(生产者-消费者模式,通过一个消息数组来进行通信),那就没必要把该共享资源定义成成员变量,而是采用ThreadLocal来处理这个变量,使得它拥有成员变量的特性(类中甚至线程中全局可用)。

ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
ThreadLocal的设计本身就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题。因为自己的变量肯定不会有并发问题的。但是这样确实是避免了这个变量使用过程中的线程安全问题。
把一个变量的使用范围限制在一个线程内,其他线程访问不到这个变量,这样这个变量也就不会有线程安全问题。ThreadLocal 是以空间换取线程安全,而通过加锁来实现线程安全,则是以时间为代价的。所以使用ThreadLocal 在某些情况下可能会获得更好的性能。ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本

ThreadLocal 的应用场景

spring中的service, controller, dao都是单例的,全局唯一,所有线程共用一个。但是这几个单例对象的成员变量对于每个线程来说都是不一样的,那既然sevice, controller, dao是单例的,那他们对应的成员变量应该也是全局唯一的,为了解决并发问题,一定要对这个成员变量进行加锁。有没有不加锁的方法呢,有,
1. 把上面三个对象都定义成多例的,每个线程都有一个实例对象,他们的成员变量也就不存在线程争用问题了。
2. 维持service, controller, dao 仍然是单例的,但是把他们的成员变量存在每个线程里,而不是存在这三个实例里面。把这个成员变量变量变成线程的变量,而不是实例的变量,这就要使用threadlocal了。threadlocal可以把一个变量变成线程的变量。

举个栗子

spring中Dao对象必须包含一个数据库的连接Connection, 因为不能给每个方法都new 一个connection对象,所以这个connection必须定义成一个dao的成员变量,让所有dao方法都使用同一个connection, 但是问题来了,因为dao是单例的,所以dao的成员变量connection必定也是全局唯一的,所以每个线程在使用connection的时候肯定会存在一个争用问题,这样这个connection就存在一个并发问题。有没有解决办法呢?有
1. 把dao对象声明成多例的并connection声明成dao的成员变量。这样每个线程都能获取到一个dao实例,使用的也是不同的connection,connection的使用就不会有线程安全问题
2. 仍然保持dao对象是单例的,但是不把connection定义成dao的成员变量,而是定义成线程的变量。这样同一个线程的dao对象的所有方法都可以使用同一个connection, 并且connection不会有线程安全问题。而把把变量定义成线程的变量就是靠threadlocal来完成的。
Thread到底是什么?他到底是怎么实现把变量存到线程里面的?这又会带来哪些问题?下面一一揭晓

threadlocal是什么

先看一张CyC2018大佬画的一张 关于 Thread、ThreadLocalMap、ThreadLocal 三个对象的关系图
可能你现在对这个图不太理解,不太看得明白,没关系,继续往下看。

ThreadLocal 原理

每个Thread类中有一个成员变量 threadlocals , 这个成员变量是一个 ThreadLocalMap的类型对象,
我们按住 Ctrl 键鼠标点击 ThreadLocalMap , 进入查看ThreadLocalMap 的底层实现
可以看到一个 Entry[] 数组,
每个Entry的底层实现为
可以看出,每个 Entry 键值对的键是 一个ThreadLocal 对象,值是一个 Object 对象。
这个ThreadLocalMap 类提供了set() 方来设置添加键值对, 提供了get()方法来获取键值对,提供了remove()方法来删除键值对,看到这些方法,是不是觉得和 ThreadLocal 对象很像,因为ThreadLocal 类也提供了这个三个方法,分别用来存储对象值,获取对象值,以及移除对象值。
我们来看ThreadLocal 类中这三个方法的底层实现
当我用调用ThreadLcoal对象的set()方法时, ThreadLocal对象会获取到当前当前线程的引用,根据这个引用获取到线程的成员ThreadLocalMap对象,然后后调用ThreadLocalMap对象的set方法存储到这个Map中。看似我们是把数据存储在了ThreadLcoal对象中,但是实际上我们是把数据存储在当前线程的ThreadLocalMap中。而threadlocal只是用来在线程中查找这个对象而已
ThreadLocal的get()方法也是类似,先获取当前线程对象引用,然后获取这个线程的成员对象ThreadLocalMap,以 ThreadLocal 引用为键,取出这个键值对中的值。
remove方法也是先获取当前线程对象引用,然后获取这个线程的成员对象ThreadLocalMap,最后移除以 ThreadLocal 引用为键的键值对。
看到这里,应该很清楚,ThreadLocal 的线程安全原理了。
ThreadLocal 的set(), get(), remove()方法实际上在操作当前线程成员变量 threadlocals, 这个变量的类型是一个ThreadLocalMap, 所以当我们往ThreadLocal中添加值实际上是把值添加到了当前线程中,从 ThreadLocal 对象中取值实际上是从当前线程中取值,从ThreadLocal 对象中移除值实际上是从把这个值从当前线程中移除,所以一切操作都是在操作当前线程中的值,threadlocal在这里只是相当于一个索引作用。那么对 ThreadLocal 中存储的对象进行操作当然就是线程安全的了,因为始终都是操作的当前线程,不涉及到其他线程,当然就不会线程不安全了。
现在再回过头去看文章开头的关系图,是不是觉得豁然开朗了~~

注意:

因为每个健值在ThreadMap中是唯一的,它唯一标识了一个健值对,所以我们在ThreadLocalMap中不能存储多个健值相等的键值对,而因为这个ThreadLocalMap是以ThreadLocal对象引用为健值,所以一个ThreadLocalMap对象只能存储一个以同一个ThreadLocal对象引用为键的键值对,也就是每个线程对同一个ThreadLocal对象,只能存储一个数据对象。

ThreadLocal的内存泄漏问题

再次查看一下 Entry 类的定义
可以看到,Entry 继承自WeakReference<ThreadLocal<?>>Entry的 key是ThreadLocal对象引用,这个引用是一个弱引用。当没指向 key 的强引用后,该key就会被垃圾收集器回收。

在ThreadLocalMap中,entry的key是弱引用,value仍然是一个强引用。当某一条线程中的ThreadLocal使用完毕,没有强引用指向它的时候,这个key指向的对象就会被垃圾收集器回收,从而这个key就变成了null;所以entry就变成了(null, value), 而entry 和 value 都是强引用,并且只要entry还在,value就一直存在。所以如果我们不手动清理掉这些键为空的entry, 在线程执行完毕之前,这个entry就一直处于内存泄漏的状态。线程生命周期越长,内存泄漏的就越多。

解决办法:

不过不用担心,ThreadLocal提供了这个问题的解决方案。

每次操作set、get、remove操作时,会相应调用 ThreadLocalMap 的三个方法,ThreadLocalMap的三个方法在每次被调用时 都会直接或间接调用一个 expungeStaleEntry() 方法,这个方法会将key为null的 Entry 删除,从而避免内存泄漏。

那么问题又来了,如果一个线程运行周期较长,而且将一个大对象放入LocalThreadMap后便不再调用set、get、remove方法仍然有可能key的弱引用被回收后,引用没有被回收,此时该仍然可能会导致内存泄漏。

这个问题确实存在,没办法通过ThreadLocal解决,而是需要程序员在完成ThreadLocal的使用后要养成手动调用remove的习惯,从而避免内存泄漏。

既然弱引用会导致内存泄漏,那ThreadLocalMap为什么对ThreadLocal的引用要设置成弱引用?

为了尽快回收这个线程变量,因为这个线程变量可能使用场景不是特别多,所以希望使用完后能尽快被释放掉。因为线程拥有的资源越多,就越臃肿,线程切换的开销就越大,所以希望尽量降低线程拥有的资源量。

参考:

Cyc2018

ThreadLocal内存泄漏原因以及避免方案
揭秘ThreadLocal
手撕面试题ThreadLocal!!!
深入理解ThreadLocal
ThreadLocal的原理以及在Spring中的应用(好文,建议阅读完上面几遍文章后再来看此文)
posted @ 2020-09-23 18:31  Lucky小黄人^_^  阅读(3166)  评论(0编辑  收藏  举报