• 深入并发之(二) ThreadLocal源码与内存泄漏相关分析


    深入并发二 ThreadLocal源码与内存泄漏相关分析

    这篇文章的主要内容是介绍ThreadLocal类使用方法,源码实现,以及实际应用。ThreadLocal实际上是在多线程编程的过程中,每个线程用来保存局部变量的一个类,用这个类保存的变量在属于各个线程独有,不会互相影响,那么我们就可以实现不同线程保存同一个变量的不同值。

    ThreadLocal的使用

    ThreadLocal的使用十分方便,下面给出一个使用的例子,实现同一个变量在不同线程中有着不同的值,同时,这两个值不互相影响。

    public static void main(String[] args) {
           ThreadLocal<Integer> value = ThreadLocal.withInitial(() -> {
               return 0;
           });
           
           new Thread(() -> {
               value.set(10);
               //Thread-0 10
               System.out.println(Thread.currentThread().getName() + " " + value.get());
           }).start();
           
           new Thread(() -> {
               //Thread-1 0
               System.out.println(Thread.currentThread().getName() + " " + value.get());
               value.set(3);
               //Thread-1 3
               System.out.println(Thread.currentThread().getName() + " " + value.get());
           }).start();
       }
    

    上面的代码中,我们定义了一个变量value,这个变量在不同线程中有不同的值,所以我们使用ThreadLocal,初始化这个值为0。上面的代码十分简单,就不做详细讲解了。

    ThreadLocal源码分析

    下面我们来分析一下ThreadLocal的底层实现。

    实际上,每个Thread对象都持有一个ThreadLocalMap的对象,里面保存了所有ThreadLocal变量,这个map的key是ThreadLocal变量对象,而值就是这个线程中ThreadLocal对应的值。

    ThreadLocalMap实际使ThreadLocal的一个静态内部类。

    下面我们先来分析方法set()

    public void set(T value) {
    	//获取当前线程
        Thread t = Thread.currentThread();
        //获取线程所持有的map对象
        ThreadLocalMap map = getMap(t);
        if (map != null)
        	//以当前ThreadLocal为key,将value值加入map中
            map.set(this, value);
        else
        	//如果map对象还没有,那么调用初始化方法,并且将值插入
            createMap(t, value);
    }
    
    ThreadLocalMap getMap(Thread t) {
    	//获取线程对象持有的map对象
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
    	//初始化threadLocals
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    

    然后我们来分析一下get()方法

    public T get() {
    	//获取当前线程
        Thread t = Thread.currentThread();
        //获取线程所持有的map对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	//取出map中key为当前ThreadLocal对象的Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            //如果存在,直接返回value
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果上面没有返回,证明还没有赋值,那么调用初始化的方法
        return setInitialValue();
    }
    
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
    

    在这里我们可以看到如果我们调用过ThreadLocal对象的set方法给对象赋值的话,这是调用get方法去取值,会调用方法setInitialValue。所以,一般我们在初始化ThreadLocal对象的时候,会重写方法initialValue(),这样就不会发生get方法返回值为null的情况。

    同时在java8之后,我们也可以采用最开始的例子中的方法来初始化ThreadLocal对象。

    ThreadLocal<Integer> value = ThreadLocal.withInitial(() -> {
    	return 0;
    });
    

    ThreadLocalMap分析与ThreadLocal导致内存泄露的问题分析

    这里我们不去分析ThreadLocalMap中方法的具体实现,它的大部分功能和一个普通的map相似,我们主要是要分析一下ThreadLocal导致内存泄漏的原因。

    首先,给出关键部分的代码

    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
    
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    

    注意,ThreadLocalMap中的key实际上是ThreadLocal对象的弱引用。

    那么什么是弱引用呢,既然有弱引用必然就有强引用。

    实际上强引用就是我们正常使用new关键字创建的引用,** 弱引用指的其实是WeakReference关键字包裹的引用,在GC的过程中,如果一个对象只有弱引用指向它的时候,这个对象就已经可以被GC回收了,而一个强引用只有当所有引用都不存在的时候才可以被回收。**

    那么,在map中为什么要使用弱引用这种方式呢?请大家想想一种情况,我们有一个对象,这个对象有一个引用A,并且这个对象作为map的key存在,那么当我们不再使用这个对象的时候,我们将引用置为null,这是,假设map中的引用是强引用,那么由于map中依然有这个对象的引用,那么这个对象不能够被GC回收,这显然不是我们想要看到的场景,所以,一般来说map中的key一般使用弱引用,这样,当对象只有这一个引用的时候就可以及时被GC回收。

    • 因此,我们就有HashMapWeakHashMap两个类,大家可以后续了解一下,两者的主要区别就在于key是强引用还是弱引用。 *

    下面转回正题,关于ThreadLocal导致内存泄漏的问题。

    在这里,key值实际上是ThreadLocal变量的弱引用,所以当我们的key变为空的时候这个引用就不存在了,那么我们也就无从得到value的值,这是value的值就变成了无法访问的值。

    ThreadLocalMap中实际上已经考虑到了这个问题,当我们调用ThreadLocal中set、get和remove方法的时候,实际上是会检查key为null的情况,将这些内容清掉。

    当线程的生命周期结束的时候后,实际上所有的ThreadLocalMap都会被回收,因此,这种情况下不会造成内存泄漏。

    这里引用StackOverFlow中一位答主给出的情况,详情见 java - ThreadLocal & Memory Leak - Stack Overflow

    这里给出翻译。

    举一个例子:
    有一个服务器有一个线程池,这些线程会一直存活知道服务器停止。
    一个web应用在一个类中使用了一个static的ThreadLocal来存放一些线程局部变量,这个变量是web应用中里一个类的对象(SomeClass)。这些操作实在一个线程中进行的。
    根据定义,一个ThreadLocal的引用会一直存活,知道拥有这个对象的线程死亡或者ThreadLocal对象本身是不可达的。
    如果web应用在关闭之前没有成功清除ThreadLocal的引用,那么这时会发生十分糟糕的事情:
    因为线程不会死亡,并且ThreadLocal对象依然指向着的引用是static的,那么,虽然应用已经停止了,ThreadLocal对象依然指向着SomeClass的对象(一个web应用中的类)
    这种情况的结果就是,web应用中的classloader不会被GC,这就意味着web应用中所有的类(以及所有的静态类)都仍然被装载(这会影响到PermGen)
    每一次reload应用都会增加PermGen的使用,这样就会导致permgen leak

    相信上面的解释已经十分清晰了。下面给出tomcat中出现的例子,这个bug已经被官方修复了。

    MemoryLeakProtection - Tomcat Wiki

    至此,我们对ThreadLocal的了解已经十分深入了,在我们使用ThreadLocal类的时候,一定要十分注意,防止发生内存泄漏。

  • 相关阅读:
    C++ *this与this的区别(系个人转载,个人再添加相关内容)
    C++ 友元(系转载多人博客,添加个人见解)
    C++模板详解(系转载,但是个人添加了一些内容)
    实验三:klee的执行重现机制(示例分析)
    klee错误汇报二:KLEE的optimize选项的一个困惑
    KLEE错误汇报一:One phenomenon after the execution using klee
    实验一:使用符号执行工具klee对软件进行破解(来自于klee官网)
    个人发现的createProcess调用漏洞
    docker运行时设置redis密码并替换redis默认的dump.rdb
    saltstack入门个人笔记
  • 原文地址:https://www.cnblogs.com/qmlingxin/p/9412061.html
Copyright © 2020-2023  润新知