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

背景

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

分析

结构

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

Java代码   收藏代码
  1. public class ThreadLocal {  
  2.        static class ThreadLocalMap {  
  3.               static class Entry extends WeakReference {  
  4.                      Object value;  
  5.                      Entry(ThreadLocal k, Object v) {  
  6.                            super(k);  
  7.                            value = v;  
  8.                      }  
  9.                      private ThreadLocal.ThreadLocalMap.Entry[] table;  
  10.               }  
  11.        }  
  12. }  

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

流程

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

Java代码   收藏代码
  1. public void set(T value) {  
  2.     Thread t = Thread.currentThread();  
  3.     ThreadLocalMap map = getMap(t);  
  4.     if (map != null)  
  5.         map.set(this, value);  
  6.     else  
  7.         createMap(t, value);  
  8. }  
当线程首次调用set方法,并不能获取到ThreadLocalMap,于是ThreadLocalMap被创建
Java代码   收藏代码
  1. void createMap(Thread t, T firstValue) {  
  2.     t.threadLocals = new ThreadLocalMap(this, firstValue);  
  3. }  
  4.   
  5. ThreadLocalMap(ThreadLocal firstKey, Object firstValue){  
  6.    table = new Entry[INITIAL_CAPACITY];  
  7.    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  
  8.    table[i] = new Entry(firstKey, firstValue);  
  9.    size = 1;  
  10.    setThreshold(INITIAL_CAPACITY);  
  11. }  

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

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

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

 

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

结论

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

问题

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

Java代码   收藏代码
  1.     public static void testThreadLocalExist(){  
  2.         ExecutorService service = Executors.newSingleThreadExecutor();  
  3.         for (int i = 0; i < 10; i++) {  
  4.             if(i == 0){  
  5.                 service.execute(new Runnable() {  
  6.                     public void run() {  
  7.                         System.out.println("Thread id is " + Thread.currentThread().getId());  
  8.                         threadLocal.set("variable");  
  9.                     }  
  10.                 });  
  11.             } else if(i > 0){  
  12.                 service.execute(new Runnable() {  
  13.                     public void run() {  
  14.                         if("variable".equals(threadLocal.get())){  
  15.                             System.out.println("Thread id " + Thread.currentThread().getId() + " got it !");  
  16.                         }  
  17.                     }  
  18.                 });  
  19.             }  
  20.         }  
  21.     }  
  22.   
  23. 输出:  
  24. Thread id is 9  
  25. Thread id 9 got it !  
  26. Thread id 9 got it !  
  27. Thread id 9 got it !  
  28. Thread id 9 got it !  
  29. Thread id 9 got it !  
  30. Thread id 9 got it !  
  31. Thread id 9 got it !  
  32. Thread id 9 got it !  
  33. Thread id 9 got it !  
 如上测试,我初始化了一个线程数量为1的线程池,为了保证每次线程池中获取到的都是同一个线程
那么根据这个测试可以看出,当线程从线程池中再次被调用的时候,这个"变量副本"是可以获取到的,即内存可能会发生泄漏,但没有实战的情况下,无法预估其影响。
2、那么当使用线程池的情况下,出于安全起见如何避免发生内存泄漏呢?在上面的测试中,做一点小小的变化
Java代码   收藏代码
  1.     public static void testThreadLocalExist() {  
  2.         ExecutorService service = Executors.newSingleThreadExecutor();  
  3.         for (int i = 0; i < 10; i++) {  
  4.             if (i == 0) {  
  5.                 service.execute(new Runnable() {  
  6.                     public void run() {  
  7.                         System.out.println("Thread id is " + Thread.currentThread().getId());  
  8.                         threadLocal.set("variable");  
  9.                         "color: #000000;">threadLocal.remove();  
  10.                     }  
  11.                 });  
  12.             } else {  
  13.                 service.execute(new Runnable() {  
  14.                     public void run() {  
  15.                         if ("variable".equals(threadLocal.get())) {  
  16.                             System.out.println("Thread id " + Thread.currentThread().getId() + " get it !");  
  17.                         } else {  
  18.                             System.out.println("Thread id " + Thread.currentThread().getId() + " can't get it !");  
  19.                         }  
  20.                     }  
  21.                 });  
  22.             }  
  23.         }  
  24.     }  
  25. 输出:  
  26. Thread id is 9  
  27. Thread id 9 can't get it !  
  28. Thread id 9 can't get it !  
  29. Thread id 9 can't get it !  
  30. Thread id 9 can't get it !  
  31. Thread id 9 can't get it !  
  32. Thread id 9 can't get it !  
  33. Thread id 9 can't get it !  
  34. Thread id 9 can't get it !  
  35. Thread id 9 can't get it !  
 如上测试,在原来的基础上,在线程第一次运行完之前调用ThreadLocal的remove方法,然后再将线程放回线程池,这样当这个线程再次被调用时,"变量副本"已经不存在了。
当ThreadLocal在调用remove方法的时候,其实就是调用ThreadLocalMap的remove方法
Java代码   收藏代码
  1. public void remove() {  
  2.     ThreadLocalMap m = getMap(Thread.currentThread());  
  3.     if (m != null)  
  4.         m.remove(this);  
  5. }  
 那就深入看看ThreadLocalMap的remove方法吧
Java代码   收藏代码
  1. private void remove(ThreadLocal key) {  
  2.        Entry[] tab = table;  
  3.        int len = tab.length;  
  4.        int i = key.threadLocalHashCode & (len-1);  
  5.        for (Entry e = tab[i];  
  6.             e != null;  
  7.             e = tab[i = nextIndex(i, len)]) {  
  8.            if (e.get() == key) {  
  9.                e.clear();  
  10.                expungeStaleEntry(i);  
  11.                return;  
  12.            }  
  13.        }  
  14.    }  
 可以看到在清空Entry之后,又调用了expungeStaleEntry方法
Java代码   收藏代码
  1. private int expungeStaleEntry(int staleSlot) {  
  2.      Entry[] tab = table;  
  3.      int len = tab.length;  
  4.   
  5.      // expunge entry at staleSlot  
  6.      "color: #000000;">tab[staleSlot].value = null;  
  7.      tab[staleSlot] = null;  
  8.      size--;  
  9.   
  10.      // Rehash until we encounter null  
  11.      Entry e;  
  12.      int i;  
  13.      for (i = nextIndex(staleSlot, len);  
  14.           (e = tab[i]) != null;  
  15.           i = nextIndex(i, len)) {  
  16.          ThreadLocal k = e.get();  
  17.          if (k == null) {  
  18.              e.value = null;  
  19.              tab[i] = null;  
  20.              size--;  
  21.          } else {  
  22.              int h = k.threadLocalHashCode & (len - 1);  
  23.              if (h != i) {  
  24.                  tab[i] = null;  
  25.   
  26.                  // Unlike Knuth 6.4 Algorithm R, we must scan until  
  27.                  // null because multiple entries could have been stale.  
  28.                  while (tab[h] != null)  
  29.                      h = nextIndex(h, len);  
  30.                  tab[h] = e;  
  31.              }  
  32.          }  
  33.      }  
  34.      return i;  
  35.  }  
 这里对防止内存泄漏做了一些处理,请注意红色的部分,手动将value的值赋为null,让下轮gc可以回收这个value对象。
以上内容为个人分析和测试,真实情况请以实战为准。如果对以上的分析,感觉有不合理的地方请大家指出,共同学习。

你可能感兴趣的:(java)