zoukankan      html  css  js  c++  java
  • 深入并发二 ThreadLocal源码与内存泄漏相关分析 https://www.cnblogs.com/qmlingxin/p/9412061.html

    深入并发二 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类的时候,一定要十分注意,防止发生内存泄漏。

  • 相关阅读:
    CodeForces 734F Anton and School
    CodeForces 733F Drivers Dissatisfaction
    CodeForces 733C Epidemic in Monstropolis
    ZOJ 3498 Javabeans
    ZOJ 3497 Mistwald
    ZOJ 3495 Lego Bricks
    CodeForces 732F Tourist Reform
    CodeForces 732E Sockets
    CodeForces 731E Funny Game
    CodeForces 731D 80-th Level Archeology
  • 原文地址:https://www.cnblogs.com/aishangliuling/p/9414922.html
Copyright © 2011-2022 走看看