中高级阶段开发者出去面试,应该躲不开ThreadLocal相关问题,本文就常见问题做出一些解答,欢迎留言探讨。

ThreadLocal为java并发提供了一个新的思路, 它用来存储Thread的局部变量, 从而达到各个Thread之间的隔离运行。它被广泛应用于框架之间的用户资源隔离、事务隔离等。

 

但是用不好会导致内存泄漏, 本文重点用于对它的使用过程的疑难解答, 相信仔细阅读完后的朋友可以随心所欲的安全使用它。

内存泄漏原因探索

ThreadLocal操作不当会引发内存泄露,最主要的原因在于它的内部类ThreadLocalMap中的Entry的设计。

Entry继承了WeakReference<ThreadLocal<?>>,即Entry的key是弱引用,所以key'会在垃圾回收的时候被回收掉, 而key对应的value则不会被回收, 这样会导致一种现象:key为null,value有值。

key为空的话value是无效数据,久而久之,value累加就会导致内存泄漏。

复制代码
static class ThreadLocalMap {        static class Entry extends WeakReference<ThreadLocal<?>> {             Object value;              Entry(ThreadLocal<?> k, Object v) {                 super(k);                 value = v;             }         }     ... }
复制代码

 

怎么解决这个内存泄漏问题

每次使用完ThreadLocal都调用它的remove()方法清除数据。因为它的remove方法会主动将当前的key和value(Entry)进行清除。

复制代码
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(); // 清除key            expungeStaleEntry(i);  // 清除value            return;         }     } }
复制代码

 

e.clear()用于清除Entry的key,它调用的是WeakReference中的方法:this.referent = null

expungeStaleEntry(i)用于清除Entry对应的value, 这个后面会详细讲。

JDK开发者是如何避免内存泄漏的

ThreadLocal的设计者也意识到了这一点(内存泄漏), 他们在一些方法中埋了对key=null的value擦除操作。

这里拿ThreadLocal提供的get()方法举例,它调用了ThreadLocalMap#getEntry()方法,对key进行了校验和对null key进行擦除。

复制代码
private Entry getEntry(ThreadLocal<?> key) {     // 拿到索引位置    int i = key.threadLocalHashCode & (table.length - 1);     Entry e = table[i];     if (e != null && e.get() == key)         return e;     else        return getEntryAfterMiss(key, i, e); }
复制代码

 

如果key为null, 则会调用getEntryAfterMiss()方法,在这个方法中,如果k == null , 则调用expungeStaleEntry(i);方法。

复制代码
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {     Entry[] tab = table;     int len = tab.length;      while (e != null) {         ThreadLocal<?> k = e.get();         if (k == key)             return e;         if (k == null)             expungeStaleEntry(i);         else             i = nextIndex(i, len);         e = tab[i];     }     return null;     }
复制代码

 

expungeStaleEntry(i)方法完成了对key=null 的key所对应的value进行赋空, 释放了空间避免内存泄漏。

同时它遍历下一个key为空的entry, 并将value赋值为null, 等待下次GC释放掉其空间。