本篇內(nèi)容主要講解“面試官問(wèn)到ThreadLocal的問(wèn)題怎么回答”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“面試官問(wèn)到ThreadLocal的問(wèn)題怎么回答”吧!
創(chuàng)新互聯(lián)公司專(zhuān)注于橫縣網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠(chéng)為您提供橫縣營(yíng)銷(xiāo)型網(wǎng)站建設(shè),橫縣網(wǎng)站制作、橫縣網(wǎng)頁(yè)設(shè)計(jì)、橫縣網(wǎng)站官網(wǎng)定制、小程序制作服務(wù),打造橫縣網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供橫縣網(wǎng)站排名全網(wǎng)營(yíng)銷(xiāo)落地服務(wù)。
杭州某商務(wù)樓里,正發(fā)生著一起求職者和面試官的battle。
面試官:你先自我介紹一下。
安琪拉:面試官你好,我是草叢三婊,最強(qiáng)中單(妲己不服),草地摩托車(chē)車(chē)手,第21套廣播體操推廣者,火的傳人安琪拉,這是我的簡(jiǎn)歷,請(qǐng)過(guò)目。
面試官:看你簡(jiǎn)歷上寫(xiě)熟悉多線程編程,熟悉到什么程度?
安琪拉:精通。
對(duì)。。。,你沒(méi)看錯(cuò),問(wèn)就是“精通”,把666打在評(píng)論區(qū)。
面試官:
[心想] 莫不是個(gè)憨批,上來(lái)就說(shuō)自己精通,誰(shuí)把精通掛嘴上,莫不是個(gè)愣頭青嘞!
面試官:那我們開(kāi)始吧。用過(guò)Threadlocal 吧?
安琪拉:用過(guò)。
面試官:那你跟我講講 ThreadLocal 在你們項(xiàng)目中的用法吧。
安琪拉:我們項(xiàng)目屬于保密項(xiàng)目,無(wú)可奉告,你還是換個(gè)問(wèn)題吧!
面試官:那說(shuō)個(gè)不保密的項(xiàng)目,或者你直接告訴我Threadlocal 的實(shí)現(xiàn)原理吧。
安琪拉:show time。。。
安琪拉:舉個(gè)栗子,我們支付寶每秒鐘同時(shí)會(huì)有很多用戶請(qǐng)求,那每個(gè)請(qǐng)求都帶有用戶信息,我們知道通常都是一個(gè)線程處理一個(gè)用戶請(qǐng)求,我們可以把用戶信息丟到Threadlocal里面,讓每個(gè)線程處理自己的用戶信息,線程之間互不干擾。
面試官:等等,問(wèn)你個(gè)私人問(wèn)題,為什么從支付寶跑出來(lái)面試,受不了PUA了嗎?
安琪拉:PUA我,不存在的,能PUA我的人還沒(méi)出生呢!公司食堂吃膩了,想換換口味。
面試官:那你來(lái)給我講講Threadlocal是干什么的?
安琪拉:Threadlocal 主要用來(lái)做線程變量的隔離,這么說(shuō)可能不是很直觀。
還是說(shuō)前面提到的例子,我們程序在處理用戶請(qǐng)求的時(shí)候,通常后端服務(wù)器是有一個(gè)線程池,來(lái)一個(gè)請(qǐng)求就交給一個(gè)線程來(lái)處理,那為了防止多線程并發(fā)處理請(qǐng)求的時(shí)候發(fā)生串?dāng)?shù)據(jù),比如AB線程分別處理安琪拉和妲己的請(qǐng)求,A線程本來(lái)處理安琪拉的請(qǐng)求,結(jié)果訪問(wèn)到妲己的數(shù)據(jù)上了,把妲己支付寶的錢(qián)轉(zhuǎn)走了。
所以就可以把安琪拉的數(shù)據(jù)跟A線程綁定,線程處理完之后解除綁定。
面試官:那把你剛才說(shuō)的場(chǎng)景用偽代碼實(shí)現(xiàn)一下,來(lái)筆給你!
安琪拉:ok
//存放用戶信息的ThreadLocal private static final ThreadLocaluserInfoThreadLocal = new ThreadLocal<>(); public Response handleRequest(UserInfo userInfo) { Response response = new Response(); try { // 1.用戶信息set到線程局部變量中 userInfoThreadLocal.set(userInfo); doHandle(); } finally { // 3.使用完移除掉 userInfoThreadLocal.remove(); } return response; } //業(yè)務(wù)邏輯處理 private void doHandle () { // 2.實(shí)際用的時(shí)候取出來(lái) UserInfo userInfo = userInfoThreadLocal.get(); //查詢用戶資產(chǎn) queryUserAsset(userInfo); }
1.2.3 步驟很清楚了。
面試官:那你跟我說(shuō)說(shuō)Threadlocal 怎么實(shí)現(xiàn)線程變量的隔離的?
安琪拉:Oh, 這么快進(jìn)入正題,我先給你畫(huà)個(gè)圖,如下:
面試官:圖我看了,那你對(duì)著前面你寫(xiě)的代碼講一下對(duì)應(yīng)圖中流程。
安琪拉:沒(méi)問(wèn)題
首先我們通過(guò)ThreadLocal
然后我們調(diào)用userInfoThreadLocal.set(userInfo); 這里做了什么事呢?
我們把源代碼拿出來(lái),看一看就清晰了。
我們知道 Thread 類(lèi)有個(gè) ThreadLocalMap 成員變量,這個(gè)Map key是Threadlocal 對(duì)象,value是你要存放的線程局部變量。
# Threadlocal類(lèi) Threadlocal.class public void set(T value) { //獲取當(dāng)前線程Thread,就是上圖畫(huà)的Thread 引用 Thread t = Thread.currentThread(); //Thread類(lèi)有個(gè)成員變量ThreadlocalMap,拿到這個(gè)Map ThreadLocalMap map = getMap(t); if (map != null) //this指的就是Threadlocal對(duì)象 map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { //獲取線程的ThreadLocalMap return t.threadLocals; } void createMap(Thread t, T firstValue) { //初始化 t.threadLocals = new ThreadLocalMap(this, firstValue); }
# Thread類(lèi) Thread.class public class Thread implements Runnable { //每個(gè)線程都有自己的ThreadLocalMap 成員變量 ThreadLocal.ThreadLocalMap threadLocals = null; }
這里是在當(dāng)前線程對(duì)象的ThreadlocalMap中put了一個(gè)元素(Entry),key是Threadlocal對(duì)象,value是userInfo。
理解兩件事就都清楚了:
ThreadLocalMap 類(lèi)的定義在 Threadlocal中。
第一,Thread 對(duì)象是Java語(yǔ)言中線程運(yùn)行的載體,每個(gè)線程都有對(duì)應(yīng)的Thread 對(duì)象,存放線程相關(guān)的一些信息,
第二,Thread類(lèi)中有個(gè)成員變量ThreadlocalMap,你就把他當(dāng)成普通的Map,key存放的是Threadlocal對(duì)象,value是你要跟線程綁定的值(線程隔離的變量),比如這里是用戶信息對(duì)象(UserInfo)。
面試官:你剛才說(shuō)Thread 類(lèi)有個(gè) ThreadlocalMap 屬性的成員變量,但是ThreadlocalMap 的定義卻在Threadlocal 中,為什么這么做?
安琪拉:我們看下ThreadlocalMap的說(shuō)明
class ThreadLocalMap * ThreadLocalMap is a customized hash map suitable only for * maintaining thread local values. No operations are exported * outside of the ThreadLocal class. The class is package private to * allow declaration of fields in class Thread. To help deal with * very large and long-lived usages, the hash table entries use * WeakReferences for keys. However, since reference queues are not * used, stale entries are guaranteed to be removed only when * the table starts running out of space.
大概意思是ThreadLocalMap 就是為維護(hù)線程本地變量而設(shè)計(jì)的,只做這一件事情。
這個(gè)也是為什么 ThreadLocalMap 是Thread的成員變量,但是卻是Threadlocal 的內(nèi)部類(lèi)(非public,只有包訪問(wèn)權(quán)限,Thread和Threadlocal都在java.lang 包下),就是讓使用者知道ThreadLocalMap就只做保存線程局部變量這一件事的。
面試官:既然是線程局部變量,那為什么不用線程對(duì)象(Thread對(duì)象)作為key,這樣不是更清晰,直接用線程作為key獲取線程變量?
安琪拉:這樣設(shè)計(jì)會(huì)有個(gè)問(wèn)題,比如: 我已經(jīng)把用戶信息存在線程變量里了,這個(gè)時(shí)候需要新增加一個(gè)線程變量,比方說(shuō)新增用戶地理位置信息,我們ThreadlocalMap 的key用的是線程,再存一個(gè)地理位置信息,key都是同一個(gè)線程(key一樣),不就把原來(lái)的用戶信息覆蓋了嘛。Map.put(key,value) 操作熟悉吧,所以網(wǎng)上有些文章說(shuō)ThreadlocalMap使用線程作為key是瞎扯的。
面試官:那新增地理位置信息應(yīng)該怎么做?
安琪拉:新創(chuàng)建一個(gè)Threadlocal對(duì)象就好了,因?yàn)門(mén)hreadLocalMap的key是Threadlocal 對(duì)象,比如新增地理位置,我就再 Threadlocal < Geo> geo = new Threadlocal(), 存放地理位置信息,這樣線程的ThreadlocalMap里面會(huì)有二個(gè)元素,一個(gè)是用戶信息,一個(gè)是地理位置。
面試官:ThreadlocalMap 是什么數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的?
安琪拉:跟HashMap 一樣,也是數(shù)組實(shí)現(xiàn)的。
代碼如下:
class ThreadLocalMap { //初始容量 private static final int INITIAL_CAPACITY = 16; //存放元素的數(shù)組 private Entry[] table; //元素個(gè)數(shù) private int size = 0; }
table 就是存儲(chǔ)線程局部變量的數(shù)組,數(shù)組元素是Entry類(lèi),Entry由key和value組成,key是Threadlocal對(duì)象,value是存放的對(duì)應(yīng)線程變量
我們前面舉得例子,數(shù)組存儲(chǔ)結(jié)構(gòu)如下圖:
面試官:ThreadlocalMap 發(fā)生hash沖突怎么辦?跟HashMap 有什么區(qū)別?
安琪拉:【心想】第一次碰到有問(wèn)ThreadlocalMap哈希沖突的,這個(gè)面試越來(lái)越有意思了。
說(shuō)道:有區(qū)別的,對(duì)待哈希沖突,HashMap采用的鏈表 + 紅黑樹(shù)的形式,如下圖,鏈表長(zhǎng)度過(guò)長(zhǎng)(>8) 就會(huì)轉(zhuǎn)成紅黑樹(shù):
ThreadlocalMap既沒(méi)有鏈表,也沒(méi)有紅黑樹(shù),采用的是鏈地址法, 鏈地址法就是如果發(fā)生沖突,ThreadlocalMap直接往后找相鄰的下一個(gè)節(jié)點(diǎn),如果相鄰節(jié)點(diǎn)為空,直接存進(jìn)去,如果不為空,繼續(xù)往后找,直到找到空的,把元素放進(jìn)去,或者元素個(gè)數(shù)超過(guò)數(shù)組長(zhǎng)度閾值,進(jìn)行擴(kuò)容。
如下圖:還是以之前的例子講解,ThreadlocalMap 數(shù)組長(zhǎng)度是4,現(xiàn)在存地理位置的時(shí)候發(fā)生hash沖突(位置1已經(jīng)有數(shù)據(jù)),那就把往后找,發(fā)現(xiàn)2 這個(gè)位置為空,就直接存放在2這個(gè)位置。
源代碼(如果閱讀起來(lái)困難,可以看完后文回過(guò)頭來(lái)閱讀):
private void set(ThreadLocal> key, Object value) { Entry[] tab = table; int len = tab.length; // hashcode & 操作其實(shí)就是 %數(shù)組長(zhǎng)度取余數(shù),例如:數(shù)組長(zhǎng)度是4,hashCode % (4-1) 就找到要存放元素的數(shù)組下標(biāo) int i = key.threadLocalHashCode & (len-1); //找到數(shù)組的空槽(=null),一般ThreadlocalMap存放元素不會(huì)很多 for (Entry e = tab[i]; e != null; //找到數(shù)組的空槽(=null) e = tab[i = nextIndex(i, len)]) { ThreadLocal> k = e.get(); //如果key值一樣,算是更新操作,直接替換 if (k == key) { e.value = value; return; } //key為空,做替換清理動(dòng)作,這個(gè)后面聊WeakReference的時(shí)候講 if (k == null) { replaceStaleEntry(key, value, i); return; } } //新new一個(gè)Entry tab[i] = new Entry(key, value); //數(shù)組元素個(gè)數(shù)+1 int sz = ++size; //如果沒(méi)清理掉元素或者存放元素個(gè)數(shù)超過(guò)數(shù)組閾值,進(jìn)行擴(kuò)容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } //順序遍歷 +1 到了數(shù)組尾部,又回到數(shù)組頭部(0這個(gè)位置) private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } // get()方法,根據(jù)ThreadLocal key獲取線程變量 private Entry getEntry(ThreadLocal> key) { //計(jì)算hash值 & 操作其實(shí)就是 %數(shù)組長(zhǎng)度取余數(shù),例如:數(shù)組長(zhǎng)度是4,hashCode % (4-1) 就找到要查詢的數(shù)組地址 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; //快速判斷 如果這個(gè)位置有值,key相等表示找到了,直接返回 if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); //miss之后順序往后找(鏈地址法,這個(gè)后面再介紹) }
面試官:我看你最前面圖中畫(huà)的ThreadlocalMap 中key是 WeakReference類(lèi)型,能講講Java中有幾種類(lèi)似的引用,什么區(qū)別嗎?
安琪拉:可以
強(qiáng)引用是使用最普遍的引用。如果一個(gè)對(duì)象具有強(qiáng)引用,那垃圾回收器絕不會(huì)回收它,當(dāng)內(nèi)存空間不足時(shí),Java虛擬機(jī)寧愿拋出OutOfMemoryError錯(cuò)誤,使程序異常終止,也不會(huì)靠隨意回收具有強(qiáng)引用的對(duì)象來(lái)解決內(nèi)存不足的問(wèn)題。
如果一個(gè)對(duì)象只具有軟引用,則內(nèi)存空間充足時(shí),垃圾回收器就不會(huì)回收它;如果內(nèi)存空間不足了,就會(huì)回收這些對(duì)象的內(nèi)存。
弱引用與軟引用的區(qū)別在于:只具有弱引用的對(duì)象擁有更短暫的生命周期。在垃圾回收器線程掃描內(nèi)存區(qū)域時(shí),一旦發(fā)現(xiàn)了只具有弱引用的對(duì)象,不管當(dāng)前內(nèi)存空間足夠與否,都會(huì)回收它的內(nèi)存。不過(guò),由于垃圾回收器是一個(gè)優(yōu)先級(jí)很低的線程,因此不一定會(huì)很快發(fā)現(xiàn)那些只具有弱引用的對(duì)象。
虛引用顧名思義,就是形同虛設(shè)。與其他幾種引用都不同,虛引用并不會(huì)決定對(duì)象的生命周期。如果一個(gè)對(duì)象僅持有虛引用,那么它就和沒(méi)有任何引用一樣,在任何時(shí)候都可能被垃圾回收器回收。
妥妥的八股文啊!尷尬(─.─|||。
面試官:那你能講講為什么ThreadlocalMap 中key 設(shè)計(jì)成 WeakReference(弱引用)類(lèi)型嗎?
安琪拉:可以的,為了盡最大努力避免內(nèi)存泄漏。
面試官:能詳細(xì)講講嗎?為什么是盡最大努力,你前面也講被WeakReference 引用的對(duì)象會(huì)直接被GC(內(nèi)存回收器) 回收,為什么不是直接避免了內(nèi)存泄漏呢?
安琪拉:我們還是看下下面這張圖:
private static final ThreadLocaluserInfoThreadLocal = new ThreadLocal<>(); userInfoThreadLocal.set(userInfo);
這里的引用關(guān)系是userInfoThreadLocal 引用了ThreadLocal對(duì)象,這是個(gè)強(qiáng)引用,ThreadLocal對(duì)象同時(shí)也被ThreadlocalMap的key引用,這是個(gè)WeakReference引用,我們前面說(shuō)GC要回收ThreadLocal對(duì)象的前提是它只被WeakReference引用,沒(méi)有任何強(qiáng)引用。
為了方便大家理解弱引用,我寫(xiě)了段Demo程序
public static void main(String[] args) { Object angela = new Object(); //弱引用 WeakReference
可以看到一旦一個(gè)對(duì)象只被弱引用引用,GC的時(shí)候就會(huì)回收這個(gè)對(duì)象。
所以只要ThreadLocal對(duì)象如果還被 userInfoThreadLocal(強(qiáng)引用) 引用著,GC是不會(huì)回收被WeakReference引用的對(duì)象的。
面試官:那既然ThreadLocal對(duì)象有強(qiáng)引用,回收不掉,干嘛還要設(shè)計(jì)成WeakReference類(lèi)型呢?
安琪拉:ThreadLocal的設(shè)計(jì)者考慮到線程往往生命周期很長(zhǎng),比如經(jīng)常會(huì)用到線程池,線程一直存活著,根據(jù)JVM 根搜索算法,一直存在 Thread -> ThreadLocalMap -> Entry(元素)這樣一條引用鏈路, 如下圖,如果key不設(shè)計(jì)成WeakReference類(lèi)型,是強(qiáng)引用的話,就一直不會(huì)被GC回收,key就一直不會(huì)是null,不為null Entry元素就不會(huì)被清理(ThreadLocalMap是根據(jù)key是否為null來(lái)判斷是否清理Entry)
所以ThreadLocal的設(shè)計(jì)者認(rèn)為只要ThreadLocal 所在的作用域結(jié)束了工作被清理了,GC回收的時(shí)候就會(huì)把key引用對(duì)象回收,key置為null,ThreadLocal會(huì)盡力保證Entry清理掉來(lái)最大可能避免內(nèi)存泄漏。
來(lái)看下代碼:
//元素類(lèi) static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */ Object value; //key是從父類(lèi)繼承的,所以這里只有value Entry(ThreadLocal> k, Object v) { super(k); value = v; } } //WeakReference 繼承了Reference,key是繼承了范型的referent public abstract class Reference { //這個(gè)就是被繼承的key private T referent; Reference(T referent) { this(referent, null); } }
Entry 繼承了WeakReference類(lèi),Entry 中的 key 是WeakReference類(lèi)型的,在Java 中當(dāng)對(duì)象只被 WeakReference 引用,沒(méi)有其他對(duì)象引用時(shí),被WeakReference 引用的對(duì)象發(fā)生GC 時(shí)會(huì)直接被回收掉。
面試官:那如果Threadlocal 對(duì)象一直有強(qiáng)引用,那怎么辦?豈不是有內(nèi)存泄漏風(fēng)險(xiǎn)。
安琪拉:最佳實(shí)踐是用完手動(dòng)調(diào)用remove函數(shù)。
我們看下源碼:
class Threadlocal { public void remove() { //這個(gè)是拿到線程的ThreadLocalMap ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); //this就是ThreadLocal對(duì)象,移除,方法在下面 } } class ThreadlocalMap { private void remove(ThreadLocal> key) { Entry[] tab = table; int len = tab.length; //計(jì)算位置 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; } } } } //這個(gè)方法就是做元素清理 private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; //把staleSlot的value置為空,然后數(shù)組元素置為空 tab[staleSlot].value = null; tab[staleSlot] = null; size--; //元素個(gè)數(shù)-1 // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal> k = e.get(); //k 為null代表引用對(duì)象被GC回收掉了 if (k == null) { e.value = null; tab[i] = null; size--; } else { //因?yàn)樵貍€(gè)數(shù)減少了,就把后面的元素重新hash int h = k.threadLocalHashCode & (len - 1); //hash地址不相等,就代表這個(gè)元素之前發(fā)生過(guò)hash沖突(本來(lái)應(yīng)該放在這沒(méi)放在這), //現(xiàn)在因?yàn)橛性乇灰瞥耍苡锌赡茉瓉?lái)沖突的位置空出來(lái)了,重試一次 if (h != i) { tab[i] = null; //繼續(xù)采用鏈地址法存放元素 while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
面試官:你有沒(méi)有用Threadlocal的工程實(shí)際經(jīng)歷,給我講講。
安琪拉:有啊!
之前我跟你們一面面試官聊過(guò),我是怎么把支付寶后臺(tái)負(fù)責(zé)的系統(tǒng)四十幾個(gè)核心rpc接口性能大幅度提升的,下面這個(gè)就是其中一個(gè)接口切流之后的效果,其中就用到了Threadlocal。圖片
面試官:嗯,說(shuō)說(shuō)。
安琪拉:我剛才說(shuō)有四十多個(gè)接口要做技改優(yōu)化,那風(fēng)險(xiǎn)是很高的,我需要保證接口切換后業(yè)務(wù)不受影響,也叫等效切換。
流程是這樣的:
把這四十多個(gè)接口按照業(yè)務(wù)含義定義了接口常量名稱,比如接口名alipay.quickquick.follow.angela;
按照接口的流量從低到高開(kāi)始切流,提前配置中心配置好每個(gè)接口的切流比例和用戶白名單;
切流也有講究,先按照userId白名單切,再按照userId尾號(hào)切百分比,完全沒(méi)問(wèn)題再完整切;
在頂層抽象模版方法的入口通過(guò)ThreadLocal Set 接口名,把接口名塞進(jìn)去;
然后我在切流的地方通過(guò)ThreadLocal 獲取接口名,用于接口切流判斷切流;
面試官:最后一個(gè)問(wèn)題,如果我有很多變量都要塞到ThreadlocalMap中,那豈不是要申明很多個(gè)Threadlocal 對(duì)象?有沒(méi)有好的解決辦法。
安琪拉:我們的最佳實(shí)踐是搞個(gè)再封裝一下,把ThreadLocalMap 的value 弄成Map就好了,這樣只要一個(gè)Threadlocal 對(duì)象就好了。
面試官:能詳細(xì)講講嗎?
安琪拉:講不動(dòng)了,太累了。
面試官:講講。
安琪拉:真不想講了。
面試官:那今天先到這,您出了這個(gè)門(mén)右拐,回去等通知吧!
到此,相信大家對(duì)“面試官問(wèn)到ThreadLocal的問(wèn)題怎么回答”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!