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

背景

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

分析

结构

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

Java代码

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

 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;  
}  
  • 这个ThreadLocalMap对象会被GC回收
  • ThreadLocalMap的成员变量table所指向的对象会被gc回收,这时注意Entry是继承了WeakReference的,所以Entry对象也会被gc回收
  • value作为Entry的成员变量自然也会被gc回收

结论

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

问题

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

 public static void testThreadLocalExist(){  
         ExecutorService service = Executors.newSingleThreadExecutor();
         ThreadLocal<String> threadLocal = new ThreadLocal<>();  
         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 !");  
                         }  
                     }  
                 });  
             }  
         }  
     }  

  1. 输出:
  2. Thread id is 9
  3. Thread id 9 got it !
  4. Thread id 9 got it !
  5. Thread id 9 got it !
  6. Thread id 9 got it !
  7. Thread id 9 got it !
  8. Thread id 9 got it !
  9. Thread id 9 got it !
  10. Thread id 9 got it !
  11. Thread id 9 got it !

​ 那么根据这个测试可以看出,当线程从线程池中再次被调用的时候,这个"变量副本"是可以获取到的,即内存可能会发生泄漏,但没有实战的情况下,无法预估其影响。

​ 那么当使用线程池的情况下,出于安全起见如何避免发生内存泄漏呢?在上面的测试中,做一点小小的变化

   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");  
                         <span style="color: #000000;">threadLocal.remove();</span>  
                     }  
                 });  
             } 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  
      <span style="color: #000000;">tab[staleSlot].value = null;  
      tab[staleSlot] = null;</span>  
      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对象。

转自 https://mahl1990.iteye.com/blog/2347932

你可能感兴趣的:(深入理解ThreadLocal的"内存溢出")