写点什么

表妹和我纠结,线上系统因为一个 ThreadLocal 直接内存飙升

  • 2022 年 1 月 24 日
  • 本文字数:5445 字

    阅读完需:约 18 分钟

前言


大家对于 ThreadLocal 这一个都应该听说过的吧,不知道大家对于这个掌握的怎么样了已经


这不,我那爱学习的表妹不知道又从哪里听来了这个技术点,回家就得意洋洋地给我说,表哥,我今天又学会了一个技术点 ThreadLocal


哦,不错啊


你这态度,好像不太信的样子啊,表妹咬牙切齿地说着


没没没,我信。我表妹那么聪明伶俐,肯定会


不行,你这态度太敷衍了,不信我给你讲一遍


得,你也先别给我讲了,你把你的 Mac 拿过来,我给你写个东西


接过她的 Mac,我三下五除二给她写了一个小例子


public class ThreadPool {private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 10, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 1000; ++i) {poolExecutor.execute(new Runnable() {@Overridepublic void run() {ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();threadLocal.set(new BigObject());// 其他业务代码}});Thread.sleep(1000);}}static class BigObject {// 100Mprivate byte[] bytes = new byte[100 * 1024 * 1024];}}你先看看这段代码,给我说说你的理解


表妹眉头一皱,你这是侮辱我的智商吗,这不就是创建了一个线程池,然后使用 for 循环增加线程,往线程池里面提交一千个任务吗


这也没啥问题啊,每个任务会向 ThreadLocal 变量里面塞一个大对象,然后执行其他业务逻辑


总之,看着没啥大毛病,这就是表妹的结论


如果你觉得这段代码没啥问题,那看来你对 ThreadLocal 学的还是不够彻底啊


代码分析来,我来给你透彻的说一遍,包教包会


表妹和我纠结,线上系统因为一个 ThreadLocal 直接内存飙升先分析一下上面的代码


创建一个核心线程数和最大线程数都为 10 的线程池,保证线程池里一直会有 10 个线程在运行。


使用 for 循环向线程池中提交了 100 个任务。


定义了一个 ThreadLocal 类型的变量,Value 类型是大对象。


每个任务会向 threadLocal 变量里塞一个大对象,然后执行其他业务逻辑。


由于没有调用线程池的 shutdown 方法,线程池里的线程还是会在运行。


说个结论


上面的代码会造成内存泄漏,会让服务的内存一直很高,即使 GC 之后也不会降低太多,这不是我们想要的结果


表妹和我纠结,线上系统因为一个 ThreadLocal 直接内存飙升 ThreadLocal 存储模型在 ThreadLocal 的内部有一个静态内部类 ThreadLocalMap,这个才是真正存储对象的 Map,我们平时使用的 set 存储的值实际上是存储到这里面的


static class ThreadLocalMap {// 定义一个 table 数组,存储多个 threadLocal 对象及其 value 值 private Entry[] table;ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}// 定义一个 Entry 类,key 是一个弱引用的 ThreadLocal 对象// value 是任意对象 static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}// 省略其他}我们重点来看 set 正如上面代码所示,ThreadLocalMap 的内部实际是一个 Entry 数组,而这个 Entry 对象就是 Key 和 Value 组成


重点来了,这个 Key 就是 ThreadLocal 实例本身,这个 Value 就是我们要存储的真实的数据


大家看到这,是不是觉得很熟悉,没错,这个 ThreadLocalMap 就是一个 Map 而已,这个 Map 和普通的 Map 有两点不同之处


1、Key、Value 的存储内容的不同


2、ThreadLocalMap 的 Key 是一个弱引用类型


其实吧,第一点也不算是不同,只是这里存储的 Key 有点出乎我们的意料,这里重点的重点其实是这个第二点,也就是这个弱引用类型,大家先记着,下面说


表妹和我纠结,线上系统因为一个 ThreadLocal 直接内存飙升我们先来看一下 ThreadLocal 的 get 和 set 方法来验证一下我的说法


ThreadLocal 的 set 方法


public class ThreadLocal<T> {


public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}


ThreadLocalMap getMap(Thread t) {    return t.threadLocals;}

void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);}// 省略其他方法
复制代码


}set 的逻辑其实也是很简单的,获取当前线程的 ThreadLocalMap,然后就直接往 map 里面添加 Key 和 Value,而这个 Key 就是 this,这个 this 就是 ThreadLocal 实例本身了


value 就是我们要存储的数据


这里需要注意一下,map 的获取是需要从 Thread 类对象里面取,看一下 Thread 类的定义。


public class Thread implements Runnable {ThreadLocal.ThreadLocalMap threadLocals = null;//省略其他}ThreadLocal 的 get 方法


class ThreadLocal<T> {


public T get() {    Thread t = Thread.currentThread();    ThreadLocalMap map = getMap(t);    if (map != null) {        ThreadLocalMap.Entry e = map.getEntry(this);        if (e != null)            return (T)e.value;    }    return setInitialValue();}
复制代码


}弱引用获取当前线程的 ThreadLocalMap 实例,如果不为空,直接用当前 ThreadLocal 的实例来作为 Key 获取 Value 即可


如果 ThreadLocalMap 为空,或者根据当前的 ThreadLocal 实例获取到的 Value 为空,则执行 setInitialValue()


而 setInitialValue 的内部实现就是如果 Map 不为空,就设置键值对,为空,则创建 Map


ThreadLocal 的内部关系表妹和我纠结,线上系统因为一个 ThreadLocal 直接内存飙升这个图画得很清晰了


每个 Thread 线程会有一个 threadlocals,这是一个 ThreadLocalMap 对象


通过这个对象,可以存储线程的私有变量,就是通过 ThreadLocal 的 set 和 get 来操作


ThreadLocal 本身不是一个容器,本身不存储任何数据,实际存储数据的对象是 ThreadLocalMap 对象,操作的过程就类似于 Map 的 put 和 get


这个 ThreadLocalMap 对象就是负责 ThreadLocal 真实存储数据的对象,内部的存储结构是 Entry 数组,这个 Entry 就是存储 Key 和 Value 对象


Key 就是 ThreadLocal 实例本身,而 Value 就是我们要存储的真实数据,而我们也从上面的源码中看到了,存和取就是根据 ThreadLocal 实例来操作的


ThreadLocal 内存模型表妹和我纠结,线上系统因为一个 ThreadLocal 直接内存飙升图中左边是栈,右边是堆。线程的一些局部变量和引用使用的内存属于 Stack(栈)区,而普通的对象是存储在 Heap(堆)区。


线程运行时,我们定义的 TheadLocal 对象被初始化,存储在 Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的 ThreadLocalRef。


当 ThreadLocal 的 set/get 被调用时,虚拟机会根据当前线程的引用也就是 CurrentThreadRef 找到其对应在堆区的实例,然后查看其对用的 TheadLocalMap 实例是否被创建,如果没有,则创建并初始化。


Map 实例化之后,也就拿到了该 ThreadLocalMap 的句柄,那么就可以将当前 ThreadLocal 对象作为 key,进行存取操作。


图中的虚线,表示 key 对应 ThreadLocal 实例的引用是个弱引用。


四种引用强引用,一直活着:


类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。


若引用,回收就会死亡


被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被引用关联的对象实例。在 JDK 1.2 之后,提供了 WeakReference 类来实现弱引用。


软引用,有一次活的机会:


软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。


虚引用,也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。


一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在 JDK 1.2 之后,提供了 PhantomReference 类来实现虚引用。


ThreadLocal 中的弱引用和内存泄漏 static class ThreadLocalMap {// 定义一个 Entry 类,key 是一个弱引用的 ThreadLocal 对象// value 是任意对象 static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}}我们先来看下 Entry 的实现,Key 会被保存到弱引用 WeakReference 中


这里的 Key 作为弱引用是关键,我们分两种情况来讨论


Key 作为强引用的时候


引用 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还有 ThreadLocal 的强引用,所以如果没有进行手动删除的话,ThreadLocal 是不会被 GC 回收的,也就是会导致 Entry 的内存泄露


一句话,强引用的时候需要手动删除才会释放内存


Key 作为弱引用的时候


引用 ThreadLocal 的对象被回收了之后,由于 ThreadLocalMap 持有的是 ThreadLocal 的弱引用,即使不会手动删除这个 ThreadLocal,这个 ThreadLocal 也会被回收


前提是该对象只被弱引用所关联,别的强引用关联不到!


而 Value 则是在下一次调用 get、set、remove 的时候进行清除,才会被 GC 自动回收


一句话,弱引用是多一层屏障,无外部强引用的时候,弱引用 ThreadLocal 会被 GC 回收,但是该 ThreadLocal 对应的 Value 只有执行 set、get 和 remove 的时候才会被清除


表妹和我纠结,线上系统因为一个 ThreadLocal 直接内存飙升比较这两种情况


由于 ThreadLocalMap 的生命周期是和 Thread 一样的,因为它是 Thread 内部实现的,如果没有手动删除对应的 key,都会导致内存泄漏


而 ThreadLocal 使用弱引用,会多了一层保障,ThreadLocal 在被清理之后,也就是 Map 中的 key 会变成 null,在使用对应 value 的时候就会将这个 value 进行清除


但是!但是!但是!


使用弱引用并不代表不需要考虑内存泄漏,只是多了一层屏障而已!


表妹和我纠结,线上系统因为一个 ThreadLocal 直接内存飙升造成内存泄漏的根源就是:ThreadLocalMap 和 Thread 的生命周期一样长,如果没有手动删除对应 key,就会导致相应的 value 不能及时得到清除,造成内存泄漏


我们在线上使用最多的就是线程池了,这样问题就大了


你想啊,线程池里面有 10 个活跃线程,线程一直在运行,不会停止,每次线程直接拿到过来用,然后用完之后会再次放到线程池中,此时线程并不会停止


也就是说这些线程的每一次使用都有可能产生新的 ThreadLocal,而我们使用完对应的 ThreadLocal 之后,如果不去手动执行 remove 删除相应的 key,就会导致 ThreadLocalMap 中的 Entry 一直在增加,并且内存是永远得不到释放


这本身就是一个很恐怖的事情,再要是放到 ThreadLocal 中的对象还是超大对象,那后果不堪设想


如何避免内存泄漏综合上面的分析,我们可以理解 ThreadLocal 内存泄漏的前因后果,那么怎么避免内存泄漏呢?


答案就是:每次使用完 ThreadLocal,建议调用它的 remove()方法,清除数据。


另外需要强调的是并不是所有使用 ThreadLocal 的地方,都要在最后 remove(),因为他们的生命周期可能是需要和项目的生存周期一样长的,所以要进行恰当的选择,以免出现业务逻辑错误!


ThreadLocal 的应用场景场景 1:


ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。


场景 2:


ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。


举个具体的使用例子


比如可以用于保存线程不安全的工具类,典型的需要使用的类就是 SimpleDateFormat。


在这种情况下,每个 Thread 内都有自己的实例副本,且该副本只能由当前 Thread 访问到并使用,相当于每个线程内部的本地变量,这也是 ThreadLocal 命名的含义。因为每个线程独享副本,而不是公用的,所以不存在多线程间共享的问题。


这种线程不安全的工具类如果需要在很多的线程中同时使用的话,任务数量巨大的情况下,也就是需要线程数巨多的情况下,这个不安全我们就需要让它变得安全


比如使用 Synchronized 锁,这样可以解决,但是 Synchronized 会让线程进入一个排队的状态,大大降低整体的工作效率


我们在线上一般使用线程池,ThreadLocal 再合适不过了,ThreadLocal 给每个线程维护一个自己的 simpleDateFormat 对象,这个对象在线程之间是独立的,互相没有关系的。这也就避免了线程安全问题。与此同时,simpleDateFormat 对象还不会创造过多,线程池有多少个线程,所以需要多少个对象即可。


再说一个形象的场景


表妹和我纠结,线上系统因为一个 ThreadLocal 直接内存飙升每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。


例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户 ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。


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


这其实就是类似于一种责任链的模式

用户头像

还未添加个人签名 2021.10.06 加入

还未添加个人简介

评论

发布
暂无评论
表妹和我纠结,线上系统因为一个ThreadLocal直接内存飙升