張三最近天氣很熱心情不是很好,因此他決定出去面試跟面試官聊聊天排解一下,結果剛投遞簡歷就有人約了面試。面試
我丟,什麼狀況怎麼剛投遞出去就有人約我面試了?誒。。。真煩啊,哥已經不在江湖這麼久了,江湖仍是有哥的傳說,我仍是這麼搶手的麼?太煩惱了,帥無罪。
暗自竊喜的張三來到了某東現場面試的辦公室,我丟,這面試官?不是吧,這盡是劃痕的Mac,這髮量,難道就是傳說中的架構師?
張三的心態一會兒就崩了,出來第一場面試就遇到一個頂級面試官,這誰頂得住啊。數據庫
我丟?這TM是人話?這是什麼邏輯啊,說是問多線程而後一上來就來個這麼冷門的ThreadLocal?心態崩了呀,再說你TM本身忘了不知道下去看看書麼,來我這裏找答案是什麼鬼啊...
儘管十分不情願,可是張三仍是高速運轉他的小腦殼,回憶起了ThreadLocal的種種細節...數組
面試官說實話我在實際開發過程當中用到ThreadLocal的地方不是不少,我在寫這個文章的時候還刻意去把我電腦上幾十個項目打開以後去全局搜索ThreadLocal發現除了系統源碼的使用,不多在項目中用到,不過也仍是有的。
ThreadLocal的做用主要是作數據隔離,填充的數據只屬於當前線程,變量的數據對別的線程而言是相對隔離的,在多線程環境下,如何防止本身的變量被其它線程篡改。安全
這,我都說了我不多用了,還問我,難受了呀,哦哦哦,有了想起來了,事務隔離級別。markdown
面試官你好,其實我第一時間想到的就是Spring實現事務隔離級別的源碼,這仍是當時我大學被女友甩了,一我的在圖書館哭泣的時候無心間發現的。
Spring採用Threadlocal的方式,來保證單個線程中的數據庫操做使用的是同一個數據庫鏈接,同時,採用這種方式可使業務層使用事務時不須要感知並管理connection對象,經過傳播級別,巧妙地管理多個事務配置之間的切換,掛起和恢復。cookie
Spring框架裏面就是用的ThreadLocal來實現這種隔離,主要是在TransactionSynchronizationManager這個類裏面,代碼以下所示:session
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class); private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources"); private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations"); private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal<>("Current transaction name"); ……
Spring的事務主要是ThreadLocal和AOP去作實現的,我這裏提一下,你們知道每一個線程本身的連接是靠ThreadLocal保存的就行了,繼續的細節我會在Spring章節細說的,暖麼?數據結構
來了來了,加分項來了,這個我還真遇到過,裝B的機會終於來了。
有的有的面試官,這個我會!!!多線程
以前咱們上線後發現部分用戶的日期竟然不對了,排查下來是SimpleDataFormat的鍋,當時咱們使用SimpleDataFormat的parse()方法,內部有一個Calendar對象,調用SimpleDataFormat的parse()方法會先調用Calendar.clear(),而後調用
Calendar.add(),若是一個線程先調用了add()而後另外一個線程又調用了clear(),這時候parse()方法解析的時間就不對了。架構
其實要解決這個問題很簡單,讓每一個線程都new 一個本身的 SimpleDataFormat就行了,可是1000個線程難道new1000個SimpleDataFormat?
因此當時咱們使用了線程池加上ThreadLocal包裝SimpleDataFormat,再調用initialValue讓每一個線程有一個SimpleDataFormat的副本,從而解決了線程安全的問題,也提升了性能。
還有還有,我還有,您彆着急問下一個,讓我再加點分,拖延一下面試時間。
我在項目中存在一個線程常常遇到橫跨若干方法調用,須要傳遞的對象,也就是上下文(Context),它是一種狀態,常常就是是用戶身份、任務信息等,就會存在過渡傳參的問題。
使用到相似責任鏈模式,給每一個方法增長一個context參數很是麻煩,並且有些時候,若是調用鏈有沒法修改源碼的第三方庫,對象參數就傳不進去了,因此我使用到了ThreadLocal去作了一下改造,這樣只須要在調用前在ThreadLocal中設置參數,其餘地方get一下就行了。
before void work(User user) { getInfo(user); checkInfo(user); setSomeThing(user); log(user); } then void work(User user) { try{ threadLocalUser.set(user); // 他們內部 User u = threadLocalUser.get(); 就行了 getInfo(); checkInfo(); setSomeThing(); log(); } finally { threadLocalUser.remove(); } }
我看了一下不少場景的cookie,session等數據隔離都是經過ThreadLocal去作實現的。
對了我面試官容許我再秀一下知識廣度,在Android中,Looper類就是利用了ThreadLocal的特性,保證每一個線程只存在一個Looper對象。
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); }
好的面試官,我先說一下他的使用:
ThreadLocal<String> localName = new ThreadLocal(); localName.set("張三"); String name = localName.get(); localName.remove();
其實使用真的很簡單,線程進來以後初始化一個能夠泛型的ThreadLocal對象,以後這個線程只要在remove以前去get,都能拿到以前set的值,注意這裏我說的是remove以前。
他是能作到線程間數據隔離的,因此別的線程使用get()方法是沒辦法拿到其餘線程的值的,可是有辦法能夠作到,我後面會說。
咱們先看看他set的源碼:
public void set(T value) { Thread t = Thread.currentThread();// 獲取當前線程 ThreadLocalMap map = getMap(t);// 獲取ThreadLocalMap對象 if (map != null) // 校驗對象是否爲空 map.set(this, value); // 不爲空set else createMap(t, value); // 爲空建立一個map對象 }
你們能夠發現set的源碼很簡單,主要就是ThreadLocalMap咱們須要關注一下,而ThreadLocalMap呢是當前線程Thread一個叫threadLocals的變量中獲取的。
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
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; ……
這裏咱們基本上能夠找到ThreadLocal數據隔離的真相了,每一個線程Thread都維護了本身的threadLocals變量,因此在每一個線程建立ThreadLocal的時候,實際上數據是存在本身線程Thread的threadLocals變量裏面的,別人沒辦法拿到,從而實現了隔離。
面試官這個問題問得好啊,心裏暗罵,讓我歇一會不行麼?
張三笑着回答道,既然有個Map那他的數據結構實際上是很像HashMap的,可是看源碼能夠發現,它並未實現Map接口,並且他的Entry是繼承WeakReference(弱引用)的,也沒有看到HashMap中的next,因此不存在鏈表了。
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; } } …… }
結構大概長這樣:
好呀,面試官你說。
用數組是由於,咱們開發過程當中能夠一個線程能夠有多個TreadLocal來存放不一樣類型的對象的,可是他們都將放到你當前線程的ThreadLocalMap裏,因此確定要數組來存。
至於Hash衝突,咱們先看一下源碼:
private void set(ThreadLocal<?> key, Object value) { 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)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
我從源碼裏面看到ThreadLocalMap在存儲的時候會給每個ThreadLocal對象一個threadLocalHashCode,在插入過程當中,根據ThreadLocal對象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)。
而後會判斷一下:若是當前位置是空的,就初始化一個Entry對象放在位置i上;
if (k == null) { replaceStaleEntry(key, value, i); return; }
若是位置i不爲空,若是這個Entry對象的key正好是即將設置的key,那麼就刷新Entry中的value;
if (k == key) { e.value = value; return; }
若是位置i的不爲空,並且key不等於entry,那就找下一個空位置,直到爲空爲止。
這樣的話,在get的時候,也會根據ThreadLocal對象的hash值,定位到table中的位置,而後判斷該位置Entry對象中的key是否和get的key一致,若是不一致,就判斷下一個位置,set和get若是衝突嚴重的話,效率仍是很低的。
如下是get的源碼,是否是就感受很好懂了:
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); } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; // get的時候同樣是根據ThreadLocal獲取到table的i值,而後查找數據拿到後會對比key是否相等 if (e != null && e.get() == key)。 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; }
在Java中,棧內存歸屬於單個線程,每一個線程都會有一個棧內存,其存儲的變量只能在其所屬線程中可見,即棧內存能夠理解成線程的私有內存,而堆內存中的對象對全部線程可見,堆內存中的對象能夠被全部線程訪問。
其實不是的,由於ThreadLocal實例實際上也是被其建立的類持有(更頂端應該是被線程持有),而ThreadLocal的值其實也是被線程實例持有,它們都是位於堆上,只是經過一些技巧將可見性修改爲了線程可見。
使用InheritableThreadLocal能夠實現多個線程訪問ThreadLocal的值,咱們在主線程中建立一個InheritableThreadLocal的實例,而後在子線程中獲得這個InheritableThreadLocal實例設置的值。
private void test() { final ThreadLocal threadLocal = new InheritableThreadLocal(); threadLocal.set("帥得一匹"); Thread t = new Thread() { @Override public void run() { super.run(); Log.i( "張三帥麼 =" + threadLocal.get()); } }; t.start(); }
在子線程中我是可以正常輸出那一行日誌的,這也是我以前面試視頻提到過的父子線程數據傳遞的問題。
傳遞的邏輯很簡單,我在開頭Thread代碼提到threadLocals的時候,大家再往下看看我刻意放了另一個變量:
Thread源碼中,咱們看看Thread.init初始化建立的時候作了什麼:
public class Thread implements Runnable { …… if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); …… }
我就截取了部分代碼,若是線程的inheritThreadLocals變量不爲空,好比咱們上面的例子,並且父線程的inheritThreadLocals也存在,那麼我就把父線程的inheritThreadLocals給當前線程的inheritThreadLocals。
是否是頗有意思?
你是說內存泄露麼?
這個問題確實會存在的,我跟你們說一下爲何,還記得我上面的代碼麼?
ThreadLocal在保存的時候會把本身當作Key存在ThreadLocalMap中,正常狀況應該是key和value都應該被外界強引用纔對,可是如今key被設計成WeakReference弱引用了。
我先給你們介紹一下弱引用:
只具備弱引用的對象擁有更短暫的生命週期,在垃圾回收器線程掃描它所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。
不過,因爲垃圾回收器是一個優先級很低的線程,所以不必定會很快發現那些只具備弱引用的對象。
這就致使了一個問題,ThreadLocal在沒有外部強引用時,發生GC時會被回收,若是建立ThreadLocal的線程一直持續運行,那麼這個Entry對象中的value就有可能一直得不到回收,發生內存泄露。
就好比線程池裏面的線程,線程都是複用的,那麼以前的線程實例處理完以後,出於複用的目的線程依然存活,因此,ThreadLocal設定的value值被持有,致使內存泄露。
按照道理一個線程使用完,ThreadLocalMap是應該要被清空的,可是如今線程被複用了。
在代碼的最後使用remove就行了,咱們只要記得在使用的最後用remove把值清空就行了。
ThreadLocal<String> localName = new ThreadLocal(); try { localName.set("張三"); …… } finally { localName.remove(); }
remove的源碼很簡單,找到對應的值所有置空,這樣在垃圾回收器回收的時候,會自動把他們回收掉。
key不設置成弱引用的話就會形成和entry中value同樣內存泄漏的場景。
補充一點:ThreadLocal的不足,我以爲能夠經過看看netty的fastThreadLocal來彌補,你們有興趣能夠康康。
什麼鬼,忽然這麼煽情,不是很爲難個人麼?難道是爲了鍛鍊我?難爲大師這樣爲我着想,我還一直內心暗罵他,不說了回去好好學了。
其實ThreadLocal用法很簡單,裏面的方法就那幾個,算上註釋源碼都沒多少行,我用了十多分鐘就過了一遍了,可是在我深挖每個方法背後邏輯的時候,也讓我不得不感慨Josh Bloch 和 Doug Lea的厲害之處。
在細節設計的處理其實每每就是咱們和大神的區別,我認爲不少不合理的點,在Google和本身不斷深刻了解以後才發現這纔是合理,
真的不服不行。
ThreadLocal是多線程裏面比較冷門的一個類,使用頻率比不上別的方法和類,可是經過我這篇文章,不知道你是否有新的認知呢?
我是敖丙,你知道的越多,你不知道的越多,咱們下期見!