ThreadLocal真的存在内存泄漏吗?来这里探讨真相吧


  • 背景

  • ThreadLocal应用场景

  • 代码演示

  • 自我设计

  • jdk中的设计

  • 内存泄漏问题探讨

    • 再谈`ThreadLocalMap`弱引用问题

  • 总结

  • 后续


背景

一直以项目中就经常使用ThreadLocal,但是对他的原理总是迷迷糊糊,每次想要了解,就打开网上看看别人的博客,发现写的都大同小异,然后又不太清楚,导致自己对ThreadLocal原理总是一知半解,今天总算有空来自己研究研究ThreadLocal,今天就带领大家来探讨久违的真相

ThreadLocal应用场景

这里先介绍下ThreadLocal的应用场景。直接看jdkThreadLocal源码上的注释(ps:我jdk版本11)ThreadLocal真的存在内存泄漏吗?来这里探讨真相吧可以看到作者是出自大师 Josh Bloch 和 Doug Lea 之手,这段话的说的大概就是:ThreadLocal提供线程局部变量。这些变量不同于它们的普通对应变量,访问某个变量(通过其 get 或 set 方法)都是每个线程自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的 private static 字段。这段话看的迷迷糊糊,大致意思就是ThreadLocal中的变量是每个线程独享的,在这个线程中任何地方都能获取到。应用场景大致有:

  1. 获取用户信息,我们在web中不同的层级中获取用户信息,可以将用户信息保存在一个ThreadLocal中,然后在任何地方都能获取到
  2. zull网关中的一些请求过滤链中的一下上下文直接的传递,也是通过ThreadLocal
  3. 有时候需要做一些aop拦截方法获取方法参数,也可以使用ThreadLocal

总之ThreadLocal应用场景非常之多

代码演示

这里我们使用一个简单的代码来验证ThreadLocal是线程隔离的

private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                Integer integer = threadLocal.get();
                System.out.println(Thread.currentThread().getName() + ": " + integer);
                threadLocal.set(integer + 1);
            });
        }
        executorService.shutdown();
    }

ThreadLocal真的存在内存泄漏吗?来这里探讨真相吧可以看到每个线程获取到的变量都是独立的,这里只是为了证明ThreadLocal线程间变量的独立性,实际ThreadLocal不会是这种应用场景,也不会这么使用

自我设计

假设要我们实现这种线程变量隔离的需求,我们需要怎么实现呢?一般也很简单。使用一个Map,key 为Thread,value为 值

public class MyThreadLocal {
    private static final Map<Thread, Object> map = new HashMap<>();
    public void get() {
        map.get(Thread.currentThread());
    }

}

ThreadLocal真的存在内存泄漏吗?来这里探讨真相吧这样设计的问题也很明显,不管线程有没有销毁,该线程存储的键值对都不会被回收,因为都在一个map中,除非这个map没有被人引用了。

jdk中的设计

目前来说jdk中的设计相对巧妙中,他是让每个线程拥有自己的ThreadLocalMap大致是如下方式:ThreadLocal真的存在内存泄漏吗?来这里探讨真相吧为什么会是这样的呢?我们直接通过源码来分析: 直接看ThreadLocalget方法

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

可以看到就是获取到当前线程t然后通过t去获取ThreadLocalMap,而getMap,可以看到就是调用Thread的属性ThreadLocal真的存在内存泄漏吗?来这里探讨真相吧所以通过源码我们证明了上面结构的正确性,然后我们来分析这种设计的优点是什么,就是每个线程自己销毁后ThreadLocalMap,不像我们设计的即使线程销毁了,他所拥有的变量还是,这样就避免了内存泄漏。所以这里我们得出了一个非常重要的结论:

==只要当前线程销毁,他所拥有的ThreadLocalMap一定会被回收,所以不存在内存泄漏问题==

内存泄漏问题探讨

通过上面的结论我们得出了一个很重要的结论:==只要当前线程销毁,他所拥有的ThreadLocalMap一定会被回收,所以不存在内存泄漏问题== ==所以平时我们线程使用完销毁是不会有有内存泄漏问题的==

那么网上一直流传的内存泄漏问题是否真的存在呢?其实是存在的。大家想想我们平时在做项目的时候自己创建线程的场景多吗?很明显基本没有,因为线程的创建销毁代价太过昂贵,我们一般都是使用线程池,那么问题来了,线程池的线程一般会循环利用,不会销毁。所以网上说的==内存泄漏是发生在使用线程池的场景==

再谈ThreadLocalMap弱引用问题

我们首先来看看ThreadLocal的set源码

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

短短几行,其实也很简单,就是获取到线程的ThreadLocalMap然后set一个属性,然后我们看看ThreadLocalMapset方法

  • ThreadLocalMap
private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                if (k == key) {
                    e.value = value;
                    return;
                }
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

我们看新加值的关键代码

tab[i] = new Entry(key, value);

可以看到Entry的key 就是我们的ThreadLocal,在看Entry的定义

static class Entry extends WeakReference<ThreadLocal<?>> {}

这里就看到了我们说的弱引用,即Entrykey是对ThreadLocal的弱引用,那么现在的引用状态呢,这里借鉴网上的一张图:ThreadLocal真的存在内存泄漏吗?来这里探讨真相吧可以很清晰的看到EntryThreadLocal的弱引用。

==注意这里弱引用解决的问题:首先虽然Key是对ThreadLocal,但是key并不会被回收,因为线程Thread对他是强引用,这里解决的问题是为了防止ThreadLocal自身的内存泄漏,因为如果这里key对ThreadLocal是强引用,那么在我们将栈中的ThreadLocalref变量不指向ThreadLocalThreadLocal也无法被回收,因为还有key对他是一个强引用,如果这是弱引用,ThreadLocal就可以顺利被回收==

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

这里证明比较复杂,但我还是引用网上的一段代码去证明 首先证明key 被回收的场景

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        Thread t = new Thread(()->test("abc",false));
        t.start();
        t.join();
        System.out.println("--gc后--");
        Thread t2 = new Thread(() -> test("def"true));
        t2.start();
        t2.join();
    }

    private static void test(String s,boolean isGC)  {
        try {
            new ThreadLocal<>().set(s);
            if (isGC) {
                System.gc();
            }
            Thread t = Thread.currentThread();
            Class<? extends Thread> clz = t.getClass();
            Field field = clz.getDeclaredField("threadLocals");
            field.setAccessible(true);
            Object ThreadLocalMap = field.get(t);
            Class<?> tlmClass = ThreadLocalMap.getClass();
            Field tableField = tlmClass.getDeclaredField("table");
            tableField.setAccessible(true);
            Object[] arr = (Object[]) tableField.get(ThreadLocalMap);
            for (Object o : arr) {
                if (o != null) {
                    Class<?> entryClass = o.getClass();
                    Field valueField = entryClass.getDeclaredField("value");
                    Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                    valueField.setAccessible(true);
                    referenceField.setAccessible(true);
                    System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

输出结果:

弱引用key:java.lang.ThreadLocal@433619b6,值:abc
弱引用key:java.lang.ThreadLocal@418a15e3,值:java.lang.ref.SoftReference@bf97a12
--gc后--
弱引用key:null,值:def

代码来源链接

==注意这里的ThreadLocal没有被任何变量引用== 但是我们实际的应用场景都是将ThreadLocal定义为 static final,那么我们将上面代码作一些改动

private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        Thread t = new Thread(()->test("abc",false));
        t.start();
        t.join();
        System.out.println("--gc后--");
        Thread t2 = new Thread(() -> test("def"true));
        t2.start();
        t2.join();
    }

    private static void test(String s,boolean isGC)  {
        try {
            threadLocal.set(s);
            if (isGC) {
                System.gc();
            }
            Thread t = Thread.currentThread();
            Class<? extends Thread> clz = t.getClass();
            Field field = clz.getDeclaredField("threadLocals");
            field.setAccessible(true);
            Object ThreadLocalMap = field.get(t);
            Class<?> tlmClass = ThreadLocalMap.getClass();
            Field tableField = tlmClass.getDeclaredField("table");
            tableField.setAccessible(true);
            Object[] arr = (Object[]) tableField.get(ThreadLocalMap);
            for (Object o : arr) {
                if (o != null) {
                    Class<?> entryClass = o.getClass();
                    Field valueField = entryClass.getDeclaredField("value");
                    Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                    valueField.setAccessible(true);
                    referenceField.setAccessible(true);
                    System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

输出结果:ThreadLocal真的存在内存泄漏吗?来这里探讨真相吧

可以看到我们验证的结论是正确的,也是==网上所说的key被回收为null的发生内存泄漏其实是错误的==

总结

  1. ThreadLocal只有在线程池下面才会发生内存泄漏
  2. ThreadLocalMap中的key为对ThreadLocal的弱引用,只是为了解决ThreadLocal回收问题
  3. ThreadLocal所谓的内存泄漏问题,不是说key为null导致,只是说我们在使用ThreadLocal因为线程没有被回收,而我们对该变量不想再使用了,而一直存在ThreadLocalMap没有被回收,就算发生了内存泄漏。所以在使用ThreadLocal注意使用完调用remove方法

后续

ThreadLocal这里主要是对网上讨论的内存泄漏问题做了一个必要的辟谣。还有一些核心源码也没有分析,限于篇幅,感兴趣自己可以自己去探讨,比如ThreadLocal中的黄金分割数等等 这里也推荐一篇ThreadLocal分析比较全的博客地址


原文始发于微信公众号(小奏技术):ThreadLocal真的存在内存泄漏吗?来这里探讨真相吧

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/30417.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!