欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

深入理解ThreadLocal的"内存溢出"

程序员文章站 2022-05-14 18:55:21
...

背景

对ThreadLocal的实际使用场景一直有点模糊。在code review中大家对ThreadLocal是否会出现内存泄漏问题提出不同看法。故上网一探究竟,但是发现网上的说法不一,有的说会导致内存泄漏有的说不会,很难发现实战的结晶。

分析

结构

一个简洁的ThreadLocal类的内部结构如下

public class ThreadLocal<T> {
       static class ThreadLocalMap {
              static class Entry extends WeakReference<ThreadLocal> {
                     Object value;
                     Entry(ThreadLocal k, Object v) {
                           super(k);
                           value = v;
                     }
                     private ThreadLocal.ThreadLocalMap.Entry[] table;
              }
       }
}

 ThreadLocal类中定义了一个静态内部类ThreadLocalMap,ThreadLocalMap并没有实现Map接口,而是自己"实现"了一个Map,在ThreadLocalMap内部定义了一个静态内部类Entry继承自WeakReference,寻找一下对WeakReference的记忆—当所引用的对象在JVM内不再有强引用指向时,GC后weak reference将会被自动回收。

流程

然后,我们从创建的流程来看一下

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
当线程首次调用set方法,并不能获取到ThreadLocalMap,于是ThreadLocalMap被创建
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    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);
    }

 可以看到ThreadLocalMap以当前ThreadLocal对象为key被创建,其内部存储结构如上,将key进行hash计算后,再将key和value放入Entry中,

注意一下上面t.threadLocals = new ThreadLocalMap(this, firstValue),实际上是一个Thread的成员变量在引用着这个ThreadLocalMap如下

public class Thread{
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
 所以我们可以分析,当Thread运行结束后(没有线程池):

 

  • 这个ThreadLocalMap对象会被GC回收
  • ThreadLocalMap的成员变量table所指向的对象会被gc回收,这时注意Entry是继承了WeakReference的,所以Entry对象也会被gc回收
  • value作为Entry的成员变量自然也会被gc回收

结论

这样看来,较为严谨的说法是,在不使用线程池的前提下,即使不调用remove方法,线程的"变量副本"也会被gc回收,即不会造成内存泄漏的情况。

问题

1、那在使用线程池的情况下呢?会不会出现内存泄漏的问题呢?我做了这样一个简单的小测试

    public static void testThreadLocalExist(){
        ExecutorService service = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            if(i == 0){
                service.execute(new Runnable() {
                    public void run() {
                        System.out.println("Thread id is " + Thread.currentThread().getId());
                        threadLocal.set("variable");
                    }
                });
            } else if(i > 0){
                service.execute(new Runnable() {
                    public void run() {
                        if("variable".equals(threadLocal.get())){
                            System.out.println("Thread id " + Thread.currentThread().getId() + " got it !");
                        }
                    }
                });
            }
        }
    }

输出:
Thread id is 9
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
 如上测试,我初始化了一个线程数量为1的线程池,为了保证每次线程池中获取到的都是同一个线程
那么根据这个测试可以看出,当线程从线程池中再次被调用的时候,这个"变量副本"是可以获取到的,即内存可能会发生泄漏,但没有实战的情况下,无法预估其影响。
2、那么当使用线程池的情况下,出于安全起见如何避免发生内存泄漏呢?在上面的测试中,做一点小小的变化
    public static void testThreadLocalExist() {
        ExecutorService service = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            if (i == 0) {
                service.execute(new Runnable() {
                    public void run() {
                        System.out.println("Thread id is " + Thread.currentThread().getId());
                        threadLocal.set("variable");
                        threadLocal.remove();
                    }
                });
            } else {
                service.execute(new Runnable() {
                    public void run() {
                        if ("variable".equals(threadLocal.get())) {
                            System.out.println("Thread id " + Thread.currentThread().getId() + " get it !");
                        } else {
                            System.out.println("Thread id " + Thread.currentThread().getId() + " can't get it !");
                        }
                    }
                });
            }
        }
    }
输出:
Thread id is 9
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
 如上测试,在原来的基础上,在线程第一次运行完之前调用ThreadLocal的remove方法,然后再将线程放回线程池,这样当这个线程再次被调用时,"变量副本"已经不存在了。
当ThreadLocal在调用remove方法的时候,其实就是调用ThreadLocalMap的remove方法
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
 那就深入看看ThreadLocalMap的remove方法吧
     private void remove(ThreadLocal key) {
            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)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
 可以看到在清空Entry之后,又调用了expungeStaleEntry方法
       private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
 这里对防止内存泄漏做了一些处理,请注意红色的部分,手动将value的值赋为null,让下轮gc可以回收这个value对象。
以上内容为个人分析和测试,真实情况请以实战为准。如果对以上的分析,感觉有不合理的地方请大家指出,共同学习。
相关标签: ThreadLocal 源码