在面试的时候,ThreadLocal作为高并发常用工具经常会被问到。而面试官比较喜欢问的问题有以下两个:
1、ThreadLocal是怎么实现来保证每个线程的变量副本的。
2、ThreadLocal的内存泄露是怎么产生的,怎么避免内存泄露。
首先我们来看第一个问题,实际上这个问题主要是想考察候选人是否有阅读过ThreadLocal的源码。当然阅读源码前我们得了解ThreadLocal是怎么使用的,只有使用过ThreadLocal我们才能产生对ThreadLocal是怎么做到的这样的疑问。而带着问题去阅读源码才能有一种廓然开朗的感觉。废话不多说,放码过来。
贴上threadlocal官方示例
public class ThreadId { // Atomic integer containing the next thread ID to be assigned private static final AtomicInteger nextId = new AtomicInteger(0); // Thread local variable containing each thread's ID private static final ThreadLocal threadId = new ThreadLocal() { @Override protected Integer initialValue() { return nextId.getAndIncrement(); } }; // Returns the current thread's unique ID, assigning it if necessary public static int get() { return threadId.get(); } }
上面代码主要通过从threadlocal得到该线程的id,如果当前线程没有的时候则生成一个。
通过上述例子我们会有如下疑问:
1、为什么ThreadLocal可以获取到当前线程的变量副本?
-> 猜测ThreadLocal内部有一个Map对象(Map),key为线程对象,value为我们存储的变量。
带着上述疑问,我们看下ThreadLocal的源码实现。
先看set方法。
public class ThreadLocal { public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } }
从getMap方法得知,ThreadLocal内部确实存在一个Map(ThreadLocalMap),只不过该map是线程的一个内部属性。而从createMap方法我们得知ThreadLocalMap是以ThreadLocal作为key,我们提供的值为value。
进入到Thread类源码查看,有两个ThreatLocalMap类型的属性。从注释可以看出,第一个是由ThreadLocal类维护的,第二个是由InheritableThreadLocal类维护的。而ThreadLocalMap是ThreadLocal的静态内部类。
public class Thread implements Runnable { /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; ......}
ThreadLocalMap内部维护的是一个Entry数组,而Entry数组继承WeakReference。我们知道,weakReference是当jvm内存不足时会回收只有WeakReference引用的对象。而ThreadLocalMap这样做的意图是为什么呢?这个问题会跟第二个疑问一起解答。
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } } private Entry[] table; ......}
如果是第一次看ThreadLocal源码的话,看到这里可能觉得有点绕,画个图理清一下关系。
首先创建了一个ThreadLocal对象
在一个线程里调用ThreadLocal的get方法,假设是第一次调用得到了null,这时我们在通过数据库查询得到value并调用set方法设置了进去,如果该Thread的threadlocals属性没有被初始化过则会执行ThreadLocal的createMap方法。
下次该线程执行调用get方法时就会从得到先set进去的值。
上面没有贴出get方法的代码,但是我们可以猜测出是通Thread.currentThread().threadLocals.get(this).value获取到值的。这里就不贴出来了。
第一个疑问解决了,现在看下面试问到的第二个问题。ThreadLocal的内存泄露是怎么产生的,怎么避免内存泄露?
网上搜ThreadLocal内存泄露很多文章都会说到ThreadLocal的ThreadLocalMap的Entry数组继承的是WeakReference,而WeakReference会在jvm内存不足是回收引用。当thread常驻或者使用线程池时核心线程常驻thread未回收,而ThreadLocal被回收,但是value又是强引用,因此不会被回收而存在内存泄露。当我看了网上形形色色的文章都是这样的描述后仍然云里雾里,而且也不符合我们实际的使用场景。
产生这个问题主要有以下几个方面的原因:
一方面是我们虽然使用过ThreadLocal,但是对ThreadLocal会导致的内存泄露问题场景不熟悉导致的。
另一方面是我们虽然没有正确使用ThreadLocal,但是内存泄露问题并不足于导致OOM(或者说存在内存泄露的问题,但是并不致命)。
以下分几个场景讨论ThreadLocal内存泄露的问题。
场景一:一个请求对应一个新线程
这种场景下使用ThreadLocal,Thread里的threatLocals变量会随着线程的销毁而销毁,自然也就不存在内存泄露的问题了。
场景二:存在强引用的几个ThreadLocal + 少量核心线程数的线程池
使用ThreadLocal的场景中,我们多数都是定义为static类变量,但是并不是意味着只有static修饰才是强引用,只要有被别的类进行强引用的都算,只是定义为static类变量是我们比较常用的场景(参照官方例子)。
由于ThreadLocal存在外部强引用,因此ThreadLocalMap的key不会出现null的情况,而少量核心线程数意味着变量副本不会很多。因此内存泄露的量为
s = coreThreadNums * (objectSize1 + objectSize2 + ... + objectSizeX), X = threadLocalNums
因此当线程池核心线程数不多,threadLocal数量不多和threadLocal变量副本不大时,虽然存在内存泄露,但是却不足于导致OOM。但是当threadLocal变量副本比较大时内存泄露的情况就会严重,导致OOM的可能性加剧了。
场景三:无强引用的几个ThreadLocal + 少量核心线程数的线程池
解析跟场景二类似,只是threadlocalmap的key会变为null,但是value却不会被回收。因此还是存在内存泄露的情况。
场景二和场景三还有一个前提条件是线程池运行的task对threadlocal的使用是一次性的。因为当调用threadLocal的set、get和remove方法时会将threadlocalmap里key为null的entry的value释放掉(实际调用的是ThreadLocalMap的expungeStaleEntry方法,感兴趣的去看源码)。当然使用了强引用的ThreadLocal是享受不到该好处的。
场景四:无强引用的大量ThreadLocal + 少量核心线程数的线程池
跟场景三类似,只是threadLocal作为threadlocal的key占用的内存稍微大了一点,
场景五:存在强引用的少量ThreadLocal + 大量核心线程数的线程池
解析跟场景二类型,由于每个常驻线程都有副本,因此内存泄露的情况加剧了。假设1000个核心线程,每个变量副本大小为1m,2个threadlocal则导致2g的内存泄露。
场景六:无强引用的少量ThreadLocal + 大量核心线程数的线程池
跟场景四类似。
场景七:无强引用ThreadLocal + 大量常驻线程的线程池服务于特定任务
这里的特定任务是指任务里都会用到threadlocal get或set 方法的任务,由于每次都调用了get或set方法,threadlocalmap会清理掉key为null的,但是当没有任务执行时,threadlocalmap的value仍然不会被回收,存在内存泄露。
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ThreadLocalTests { public static void main(String[] args) throws Exception{ final ThreadLocal threadLocal1 = new ThreadLocal<>(); final ThreadLocal threadLocal2 = new ThreadLocal<>(); final ExecutorService threadPool = Executors.newFixedThreadPool(1000); Thread.sleep(10000L); for (int i = 0; i < 1000; i++){ threadPool.submit(() -> { byte[] bytes = new byte[1024 * 1024]; threadLocal1.set(bytes); }); threadPool.submit(() -> { byte[] bytes = new byte[1024 * 1024]; threadLocal2.set(bytes); }); } }}
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ThreadLocalTests { public static void main(String[] args) throws Exception{ final ThreadLocal threadLocal1 = new ThreadLocal<>(); final ThreadLocal threadLocal2 = new ThreadLocal<>(); final ExecutorService threadPool = Executors.newFixedThreadPool(1000); Thread.sleep(10000L); for (int i = 0; i < 1000; i++){ threadPool.submit(() -> { byte[] bytes = new byte[1024 * 1024]; threadLocal1.set(bytes); threadLocal1.remove(); }); threadPool.submit(() -> { byte[] bytes = new byte[1024 * 1024]; threadLocal2.set(bytes); threadLocal2.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; } }}
public void clear() { this.referent = null;}
其他场景就不讨论了,可自行扩展。
这里做个总结。
无论threadlocal数量多少和常驻线程数量的多少都会导致内存泄露的问题,只是严重程度不同罢了。
那怎样才是使用ThreadLocal的正确姿势而不会导致内存泄露呢?这里举例zuul的requestContext例子。
zuul的requestContext
public class RequestContext extends ConcurrentHashMap<String, Object> { protected static final ThreadLocal extends RequestContext> threadLocal = new ThreadLocal() { @Override protected RequestContext initialValue() { try { return contextClass.newInstance(); } catch (Throwable e) { throw new RuntimeException(e); } } }; /** * unsets the threadLocal context. Done at the end of the request. */ public void unset() { threadLocal.remove(); }}
public class ZuulServlet extends HttpServlet { @Override public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException { try { init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); // Marks this request as having passed through the "Zuul engine", as opposed to servlets // explicitly bound in web.xml, for which requests will not have the same data attached RequestContext context = RequestContext.getCurrentContext(); context.setZuulEngineRan(); try { preRoute(); } catch (ZuulException e) { error(e); postRoute(); return; } try { route(); } catch (ZuulException e) { error(e); postRoute(); return; } try { postRoute(); } catch (ZuulException e) { error(e); return; } } catch (Throwable e) { error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName())); } finally { // 实际执行threadlocal的清理方法 RequestContext.getCurrentContext().unset(); } }}
只要在每次用完threadlocal后执行threadlocal的remove方法就可以清除掉变量副本,这样就不会产生内存泄露了。
ThreadLocalMap的remove方法,调用ThreadLocal的remove方法实际上是执行了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; } } }