inputstreamreader未关闭会导致oom_ThreadLocal 一定会导致内存泄露?
在面試的時(shí)候,ThreadLocal作為高并發(fā)常用工具經(jīng)常會(huì)被問(wèn)到。而面試官比較喜歡問(wèn)的問(wèn)題有以下兩個(gè):
1、ThreadLocal是怎么實(shí)現(xiàn)來(lái)保證每個(gè)線程的變量副本的。
2、ThreadLocal的內(nèi)存泄露是怎么產(chǎn)生的,怎么避免內(nèi)存泄露。
首先我們來(lái)看第一個(gè)問(wèn)題,實(shí)際上這個(gè)問(wèn)題主要是想考察候選人是否有閱讀過(guò)ThreadLocal的源碼。當(dāng)然閱讀源碼前我們得了解ThreadLocal是怎么使用的,只有使用過(guò)ThreadLocal我們才能產(chǎn)生對(duì)ThreadLocal是怎么做到的這樣的疑問(wèn)。而帶著問(wèn)題去閱讀源碼才能有一種廓然開朗的感覺(jué)。廢話不多說(shuō),放碼過(guò)來(lái)。
貼上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();?????}?}上面代碼主要通過(guò)從threadlocal得到該線程的id,如果當(dāng)前線程沒(méi)有的時(shí)候則生成一個(gè)。
通過(guò)上述例子我們會(huì)有如下疑問(wèn):
1、為什么ThreadLocal可以獲取到當(dāng)前線程的變量副本?
? ?-> 猜測(cè)ThreadLocal內(nèi)部有一個(gè)Map對(duì)象(Map),key為線程對(duì)象,value為我們存儲(chǔ)的變量。
帶著上述疑問(wèn),我們看下ThreadLocal的源碼實(shí)現(xiàn)。
先看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內(nèi)部確實(shí)存在一個(gè)Map(ThreadLocalMap),只不過(guò)該map是線程的一個(gè)內(nèi)部屬性。而從createMap方法我們得知ThreadLocalMap是以ThreadLocal作為key,我們提供的值為value。
進(jìn)入到Thread類源碼查看,有兩個(gè)ThreatLocalMap類型的屬性。從注釋可以看出,第一個(gè)是由ThreadLocal類維護(hù)的,第二個(gè)是由InheritableThreadLocal類維護(hù)的。而ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類。
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內(nèi)部維護(hù)的是一個(gè)Entry數(shù)組,而Entry數(shù)組繼承WeakReference。我們知道,weakReference是當(dāng)jvm內(nèi)存不足時(shí)會(huì)回收只有WeakReference引用的對(duì)象。而ThreadLocalMap這樣做的意圖是為什么呢?這個(gè)問(wèn)題會(huì)跟第二個(gè)疑問(wèn)一起解答。
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源碼的話,看到這里可能覺(jué)得有點(diǎn)繞,畫個(gè)圖理清一下關(guān)系。
首先創(chuàng)建了一個(gè)ThreadLocal對(duì)象
在一個(gè)線程里調(diào)用ThreadLocal的get方法,假設(shè)是第一次調(diào)用得到了null,這時(shí)我們?cè)谕ㄟ^(guò)數(shù)據(jù)庫(kù)查詢得到value并調(diào)用set方法設(shè)置了進(jìn)去,如果該Thread的threadlocals屬性沒(méi)有被初始化過(guò)則會(huì)執(zhí)行ThreadLocal的createMap方法。
下次該線程執(zhí)行調(diào)用get方法時(shí)就會(huì)從得到先set進(jìn)去的值。
上面沒(méi)有貼出get方法的代碼,但是我們可以猜測(cè)出是通Thread.currentThread().threadLocals.get(this).value獲取到值的。這里就不貼出來(lái)了。
第一個(gè)疑問(wèn)解決了,現(xiàn)在看下面試問(wèn)到的第二個(gè)問(wèn)題。ThreadLocal的內(nèi)存泄露是怎么產(chǎn)生的,怎么避免內(nèi)存泄露?
網(wǎng)上搜ThreadLocal內(nèi)存泄露很多文章都會(huì)說(shuō)到ThreadLocal的ThreadLocalMap的Entry數(shù)組繼承的是WeakReference,而WeakReference會(huì)在jvm內(nèi)存不足是回收引用。當(dāng)thread常駐或者使用線程池時(shí)核心線程常駐thread未回收,而ThreadLocal被回收,但是value又是強(qiáng)引用,因此不會(huì)被回收而存在內(nèi)存泄露。當(dāng)我看了網(wǎng)上形形色色的文章都是這樣的描述后仍然云里霧里,而且也不符合我們實(shí)際的使用場(chǎng)景。
產(chǎn)生這個(gè)問(wèn)題主要有以下幾個(gè)方面的原因:
一方面是我們雖然使用過(guò)ThreadLocal,但是對(duì)ThreadLocal會(huì)導(dǎo)致的內(nèi)存泄露問(wèn)題場(chǎng)景不熟悉導(dǎo)致的。
另一方面是我們雖然沒(méi)有正確使用ThreadLocal,但是內(nèi)存泄露問(wèn)題并不足于導(dǎo)致OOM(或者說(shuō)存在內(nèi)存泄露的問(wèn)題,但是并不致命)。
以下分幾個(gè)場(chǎng)景討論ThreadLocal內(nèi)存泄露的問(wèn)題。
場(chǎng)景一:一個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)新線程
這種場(chǎng)景下使用ThreadLocal,Thread里的threatLocals變量會(huì)隨著線程的銷毀而銷毀,自然也就不存在內(nèi)存泄露的問(wèn)題了。
場(chǎng)景二:存在強(qiáng)引用的幾個(gè)ThreadLocal?+ 少量核心線程數(shù)的線程池
使用ThreadLocal的場(chǎng)景中,我們多數(shù)都是定義為static類變量,但是并不是意味著只有static修飾才是強(qiáng)引用,只要有被別的類進(jìn)行強(qiáng)引用的都算,只是定義為static類變量是我們比較常用的場(chǎng)景(參照官方例子)。
由于ThreadLocal存在外部強(qiáng)引用,因此ThreadLocalMap的key不會(huì)出現(xiàn)null的情況,而少量核心線程數(shù)意味著變量副本不會(huì)很多。因此內(nèi)存泄露的量為?
s?=?coreThreadNums?*?(objectSize1?+?objectSize2?+?...?+?objectSizeX),?X?= threadLocalNums?因此當(dāng)線程池核心線程數(shù)不多,threadLocal數(shù)量不多和threadLocal變量副本不大時(shí),雖然存在內(nèi)存泄露,但是卻不足于導(dǎo)致OOM。但是當(dāng)threadLocal變量副本比較大時(shí)內(nèi)存泄露的情況就會(huì)嚴(yán)重,導(dǎo)致OOM的可能性加劇了。
場(chǎng)景三:無(wú)強(qiáng)引用的幾個(gè)ThreadLocal?+ 少量核心線程數(shù)的線程池
解析跟場(chǎng)景二類似,只是threadlocalmap的key會(huì)變?yōu)閚ull,但是value卻不會(huì)被回收。因此還是存在內(nèi)存泄露的情況。
場(chǎng)景二和場(chǎng)景三還有一個(gè)前提條件是線程池運(yùn)行的task對(duì)threadlocal的使用是一次性的。因?yàn)楫?dāng)調(diào)用threadLocal的set、get和remove方法時(shí)會(huì)將threadlocalmap里key為null的entry的value釋放掉(實(shí)際調(diào)用的是ThreadLocalMap的expungeStaleEntry方法,感興趣的去看源碼)。當(dāng)然使用了強(qiáng)引用的ThreadLocal是享受不到該好處的。
場(chǎng)景四:無(wú)強(qiáng)引用的大量ThreadLocal +?少量核心線程數(shù)的線程池
跟場(chǎng)景三類似,只是threadLocal作為threadlocal的key占用的內(nèi)存稍微大了一點(diǎn),
場(chǎng)景五:存在強(qiáng)引用的少量ThreadLocal +?大量核心線程數(shù)的線程池
解析跟場(chǎng)景二類型,由于每個(gè)常駐線程都有副本,因此內(nèi)存泄露的情況加劇了。假設(shè)1000個(gè)核心線程,每個(gè)變量副本大小為1m,2個(gè)threadlocal則導(dǎo)致2g的內(nèi)存泄露。
場(chǎng)景六:無(wú)強(qiáng)引用的少量ThreadLocal?+?大量核心線程數(shù)的線程池
跟場(chǎng)景四類似。
場(chǎng)景七:無(wú)強(qiáng)引用ThreadLocal +?大量常駐線程的線程池服務(wù)于特定任務(wù)
這里的特定任務(wù)是指任務(wù)里都會(huì)用到threadlocal get或set 方法的任務(wù),由于每次都調(diào)用了get或set方法,threadlocalmap會(huì)清理掉key為null的,但是當(dāng)沒(méi)有任務(wù)執(zhí)行時(shí),threadlocalmap的value仍然不會(huì)被回收,存在內(nèi)存泄露。
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;}其他場(chǎng)景就不討論了,可自行擴(kuò)展。
這里做個(gè)總結(jié)。
無(wú)論threadlocal數(shù)量多少和常駐線程數(shù)量的多少都會(huì)導(dǎo)致內(nèi)存泄露的問(wèn)題,只是嚴(yán)重程度不同罷了。
那怎樣才是使用ThreadLocal的正確姿勢(shì)而不會(huì)導(dǎo)致內(nèi)存泄露呢?這里舉例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 {????????????// 實(shí)際執(zhí)行threadlocal的清理方法 RequestContext.getCurrentContext().unset(); } }}只要在每次用完threadlocal后執(zhí)行threadlocal的remove方法就可以清除掉變量副本,這樣就不會(huì)產(chǎn)生內(nèi)存泄露了。
ThreadLocalMap的remove方法,調(diào)用ThreadLocal的remove方法實(shí)際上是執(zhí)行了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; } } }總結(jié)
以上是生活随笔為你收集整理的inputstreamreader未关闭会导致oom_ThreadLocal 一定会导致内存泄露?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: java中二进制怎么说_面试常用:说清楚
- 下一篇: mysql连接超时timeout问题