溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

面試官問到ThreadLocal的問題怎么回答

發布時間:2021-06-28 17:03:44 來源:億速云 閱讀:162 作者:chen 欄目:web開發

本篇內容主要講解“面試官問到ThreadLocal的問題怎么回答”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“面試官問到ThreadLocal的問題怎么回答”吧!

開場

杭州某商務樓里,正發生著一起求職者和面試官的battle。

面試官:你先自我介紹一下。

安琪拉:面試官你好,我是草叢三婊,最強中單(妲己不服),草地摩托車車手,第21套廣播體操推廣者,火的傳人安琪拉,這是我的簡歷,請過目。

面試官:看你簡歷上寫熟悉多線程編程,熟悉到什么程度?

安琪拉:精通。

對。。。,你沒看錯,問就是“精通”,把666打在評論區。

面試官:

[心想] 莫不是個憨批,上來就說自己精通,誰把精通掛嘴上,莫不是個愣頭青嘞!

面試官:那我們開始吧。用過Threadlocal 吧?

安琪拉:用過。

面試官:那你跟我講講 ThreadLocal 在你們項目中的用法吧。

安琪拉:我們項目屬于保密項目,無可奉告,你還是換個問題吧!

面試官:那說個不保密的項目,或者你直接告訴我Threadlocal 的實現原理吧。

正題

安琪拉:show time。。。

安琪拉:舉個栗子,我們支付寶每秒鐘同時會有很多用戶請求,那每個請求都帶有用戶信息,我們知道通常都是一個線程處理一個用戶請求,我們可以把用戶信息丟到Threadlocal里面,讓每個線程處理自己的用戶信息,線程之間互不干擾。

面試官:等等,問你個私人問題,為什么從支付寶跑出來面試,受不了PUA了嗎?

安琪拉:PUA我,不存在的,能PUA我的人還沒出生呢!公司食堂吃膩了,想換換口味。

面試官:那你來給我講講Threadlocal是干什么的?

安琪拉:Threadlocal 主要用來做線程變量的隔離,這么說可能不是很直觀。

還是說前面提到的例子,我們程序在處理用戶請求的時候,通常后端服務器是有一個線程池,來一個請求就交給一個線程來處理,那為了防止多線程并發處理請求的時候發生串數據,比如AB線程分別處理安琪拉和妲己的請求,A線程本來處理安琪拉的請求,結果訪問到妲己的數據上了,把妲己支付寶的錢轉走了。

所以就可以把安琪拉的數據跟A線程綁定,線程處理完之后解除綁定。

面試官問到ThreadLocal的問題怎么回答

面試官:那把你剛才說的場景用偽代碼實現一下,來筆給你!

安琪拉:ok

//存放用戶信息的ThreadLocal private static final ThreadLocal<UserInfo> userInfoThreadLocal = new ThreadLocal<>();  public Response handleRequest(UserInfo userInfo) {   Response response = new Response();   try {     // 1.用戶信息set到線程局部變量中     userInfoThreadLocal.set(userInfo);     doHandle();   } finally {     // 3.使用完移除掉     userInfoThreadLocal.remove();   }    return response; }      //業務邏輯處理 private void doHandle () {   // 2.實際用的時候取出來   UserInfo userInfo = userInfoThreadLocal.get();   //查詢用戶資產   queryUserAsset(userInfo); }

1.2.3 步驟很清楚了。

面試官:那你跟我說說Threadlocal 怎么實現線程變量的隔離的?

安琪拉:Oh, 這么快進入正題,我先給你畫個圖,如下:

面試官問到ThreadLocal的問題怎么回答

面試官:圖我看了,那你對著前面你寫的代碼講一下對應圖中流程。

安琪拉:沒問題

  • 首先我們通過ThreadLocal

    userInfoThreadLocal = new ThreadLocal()  初始化了一個Threadlocal 對象,就是上圖中說的Threadlocal 引用,這個引用指向堆中的ThreadLocal 對象;
  • 然后我們調用userInfoThreadLocal.set(userInfo); 這里做了什么事呢?

我們把源代碼拿出來,看一看就清晰了。

我們知道 Thread 類有個 ThreadLocalMap 成員變量,這個Map key是Threadlocal  對象,value是你要存放的線程局部變量。

# Threadlocal類 Threadlocal.class  public void set(T value) {   //獲取當前線程Thread,就是上圖畫的Thread 引用   Thread t = Thread.currentThread();    //Thread類有個成員變量ThreadlocalMap,拿到這個Map   ThreadLocalMap map = getMap(t);   if (map != null)     //this指的就是Threadlocal對象     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類 Thread.class public class Thread implements Runnable {     //每個線程都有自己的ThreadLocalMap 成員變量     ThreadLocal.ThreadLocalMap threadLocals = null; }

這里是在當前線程對象的ThreadlocalMap中put了一個元素(Entry),key是Threadlocal對象,value是userInfo。

理解兩件事就都清楚了:

ThreadLocalMap 類的定義在 Threadlocal中。

  • 第一,Thread 對象是Java語言中線程運行的載體,每個線程都有對應的Thread 對象,存放線程相關的一些信息,

  • 第二,Thread類中有個成員變量ThreadlocalMap,你就把他當成普通的Map,key存放的是Threadlocal對象,value是你要跟線程綁定的值(線程隔離的變量),比如這里是用戶信息對象(UserInfo)。

面試官:你剛才說Thread 類有個 ThreadlocalMap 屬性的成員變量,但是ThreadlocalMap 的定義卻在Threadlocal  中,為什么這么做?

安琪拉:我們看下ThreadlocalMap的說明

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 就是為維護線程本地變量而設計的,只做這一件事情。

這個也是為什么 ThreadLocalMap 是Thread的成員變量,但是卻是Threadlocal  的內部類(非public,只有包訪問權限,Thread和Threadlocal都在java.lang  包下),就是讓使用者知道ThreadLocalMap就只做保存線程局部變量這一件事的。

面試官:既然是線程局部變量,那為什么不用線程對象(Thread對象)作為key,這樣不是更清晰,直接用線程作為key獲取線程變量?

安琪拉:這樣設計會有個問題,比如:  我已經把用戶信息存在線程變量里了,這個時候需要新增加一個線程變量,比方說新增用戶地理位置信息,我們ThreadlocalMap  的key用的是線程,再存一個地理位置信息,key都是同一個線程(key一樣),不就把原來的用戶信息覆蓋了嘛。Map.put(key,value)  操作熟悉吧,所以網上有些文章說ThreadlocalMap使用線程作為key是瞎扯的。

面試官:那新增地理位置信息應該怎么做?

安琪拉:新創建一個Threadlocal對象就好了,因為ThreadLocalMap的key是Threadlocal 對象,比如新增地理位置,我就再  Threadlocal < Geo> geo = new Threadlocal(),  存放地理位置信息,這樣線程的ThreadlocalMap里面會有二個元素,一個是用戶信息,一個是地理位置。

面試官:ThreadlocalMap 是什么數據結構實現的?

安琪拉:跟HashMap 一樣,也是數組實現的。

代碼如下:

class ThreadLocalMap {  //初始容量  private static final int INITIAL_CAPACITY = 16;  //存放元素的數組  private Entry[] table;  //元素個數  private int size = 0; }

table  就是存儲線程局部變量的數組,數組元素是Entry類,Entry由key和value組成,key是Threadlocal對象,value是存放的對應線程變量

我們前面舉得例子,數組存儲結構如下圖:

面試官問到ThreadLocal的問題怎么回答

面試官:ThreadlocalMap 發生hash沖突怎么辦?跟HashMap 有什么區別?

安琪拉:【心想】第一次碰到有問ThreadlocalMap哈希沖突的,這個面試越來越有意思了。

說道:有區別的,對待哈希沖突,HashMap采用的鏈表 + 紅黑樹的形式,如下圖,鏈表長度過長(>8) 就會轉成紅黑樹:

面試官問到ThreadLocal的問題怎么回答

ThreadlocalMap既沒有鏈表,也沒有紅黑樹,采用的是鏈地址法,  鏈地址法就是如果發生沖突,ThreadlocalMap直接往后找相鄰的下一個節點,如果相鄰節點為空,直接存進去,如果不為空,繼續往后找,直到找到空的,把元素放進去,或者元素個數超過數組長度閾值,進行擴容。

如下圖:還是以之前的例子講解,ThreadlocalMap 數組長度是4,現在存地理位置的時候發生hash沖突(位置1已經有數據),那就把往后找,發現2  這個位置為空,就直接存放在2這個位置。

面試官問到ThreadLocal的問題怎么回答

源代碼(如果閱讀起來困難,可以看完后文回過頭來閱讀):

private void set(ThreadLocal<?> key, Object value) {   Entry[] tab = table;   int len = tab.length;   // hashcode & 操作其實就是 %數組長度取余數,例如:數組長度是4,hashCode % (4-1) 就找到要存放元素的數組下標   int i = key.threadLocalHashCode & (len-1);    //找到數組的空槽(=null),一般ThreadlocalMap存放元素不會很多   for (Entry e = tab[i];        e != null; //找到數組的空槽(=null)        e = tab[i = nextIndex(i, len)]) {     ThreadLocal<?> k = e.get();      //如果key值一樣,算是更新操作,直接替換     if (k == key) {       e.value = value;       return;     }   //key為空,做替換清理動作,這個后面聊WeakReference的時候講     if (k == null) {       replaceStaleEntry(key, value, i);       return;     }   }  //新new一個Entry   tab[i] = new Entry(key, value);   //數組元素個數+1   int sz = ++size;   //如果沒清理掉元素或者存放元素個數超過數組閾值,進行擴容   if (!cleanSomeSlots(i, sz) && sz >= threshold)     rehash(); }  //順序遍歷 +1 到了數組尾部,又回到數組頭部(0這個位置) private static int nextIndex(int i, int len) {   return ((i + 1 < len) ? i + 1 : 0); }  // get()方法,根據ThreadLocal key獲取線程變量 private Entry getEntry(ThreadLocal<?> key) {   //計算hash值 & 操作其實就是 %數組長度取余數,例如:數組長度是4,hashCode % (4-1) 就找到要查詢的數組地址   int i = key.threadLocalHashCode & (table.length - 1);   Entry e = table[i];   //快速判斷 如果這個位置有值,key相等表示找到了,直接返回   if (e != null && e.get() == key)     return e;   else     return getEntryAfterMiss(key, i, e); //miss之后順序往后找(鏈地址法,這個后面再介紹) }

面試官:我看你最前面圖中畫的ThreadlocalMap 中key是  WeakReference類型,能講講Java中有幾種類似的引用,什么區別嗎?

安琪拉:可以

  • 強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它,當內存空間不足時,Java虛擬機寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。

  • 如果一個對象只具有軟引用,則內存空間充足時,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。

  • 弱引用與軟引用的區別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描內存區域時,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由于垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。

  • 虛引用顧名思義,就是形同虛設。與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。

妥妥的八股文啊!尷尬(─.─|||。

面試官:那你能講講為什么ThreadlocalMap 中key 設計成 WeakReference(弱引用)類型嗎?

安琪拉:可以的,為了盡最大努力避免內存泄漏。

面試官:能詳細講講嗎?為什么是盡最大努力,你前面也講被WeakReference 引用的對象會直接被GC(內存回收器)  回收,為什么不是直接避免了內存泄漏呢?

安琪拉:我們還是看下下面這張圖:

面試官問到ThreadLocal的問題怎么回答

private static final ThreadLocal<UserInfo> userInfoThreadLocal = new ThreadLocal<>(); userInfoThreadLocal.set(userInfo);

這里的引用關系是userInfoThreadLocal  引用了ThreadLocal對象,這是個強引用,ThreadLocal對象同時也被ThreadlocalMap的key引用,這是個WeakReference引用,我們前面說GC要回收ThreadLocal對象的前提是它只被WeakReference引用,沒有任何強引用。

為了方便大家理解弱引用,我寫了段Demo程序

public static void main(String[] args) {   Object angela = new Object();   //弱引用   WeakReference<Object> weakReference = new WeakReference<>(angela);   //angela和弱引用指向同一個對象   System.out.println(angela);//java.lang.Object@4550017c   System.out.println(weakReference.get());//java.lang.Object@4550017c    //將強引用angela置為null,這個對象就只剩下弱引用了,內存夠用,弱引用也會被回收   angela = null;    System.gc();//內存夠用不會自動gc,手動喚醒gc   System.out.println(angela);//null   System.out.println(weakReference.get());//null }

可以看到一旦一個對象只被弱引用引用,GC的時候就會回收這個對象。

所以只要ThreadLocal對象如果還被  userInfoThreadLocal(強引用) 引用著,GC是不會回收被WeakReference引用的對象的。

面試官:那既然ThreadLocal對象有強引用,回收不掉,干嘛還要設計成WeakReference類型呢?

安琪拉:ThreadLocal的設計者考慮到線程往往生命周期很長,比如經常會用到線程池,線程一直存活著,根據JVM 根搜索算法,一直存在 Thread  -> ThreadLocalMap -> Entry(元素)這樣一條引用鏈路,  如下圖,如果key不設計成WeakReference類型,是強引用的話,就一直不會被GC回收,key就一直不會是null,不為null  Entry元素就不會被清理(ThreadLocalMap是根據key是否為null來判斷是否清理Entry)

面試官問到ThreadLocal的問題怎么回答

所以ThreadLocal的設計者認為只要ThreadLocal  所在的作用域結束了工作被清理了,GC回收的時候就會把key引用對象回收,key置為null,ThreadLocal會盡力保證Entry清理掉來最大可能避免內存泄漏。

來看下代碼:

//元素類 static class Entry extends WeakReference<ThreadLocal<?>> {   /** The value associated with this ThreadLocal. */   Object value; //key是從父類繼承的,所以這里只有value    Entry(ThreadLocal<?> k, Object v) {     super(k);     value = v;   } }  //WeakReference 繼承了Reference,key是繼承了范型的referent public abstract class Reference<T> {   //這個就是被繼承的key   private T referent;    Reference(T referent) {     this(referent, null);   } }

Entry 繼承了WeakReference類,Entry 中的 key 是WeakReference類型的,在Java 中當對象只被  WeakReference 引用,沒有其他對象引用時,被WeakReference 引用的對象發生GC 時會直接被回收掉。

面試官:那如果Threadlocal 對象一直有強引用,那怎么辦?豈不是有內存泄漏風險。

安琪拉:最佳實踐是用完手動調用remove函數。

我們看下源碼:

class Threadlocal {   public void remove() {       //這個是拿到線程的ThreadLocalMap       ThreadLocalMap m = getMap(Thread.currentThread());       if (m != null)         m.remove(this); //this就是ThreadLocal對象,移除,方法在下面   } }  class ThreadlocalMap {   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;       }    }  } }  //這個方法就是做元素清理 private int expungeStaleEntry(int staleSlot) {   Entry[] tab = table;   int len = tab.length;    //把staleSlot的value置為空,然后數組元素置為空   tab[staleSlot].value = null;   tab[staleSlot] = null;   size--; //元素個數-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代表引用對象被GC回收掉了     if (k == null) {       e.value = null;       tab[i] = null;       size--;     } else {       //因為元素個數減少了,就把后面的元素重新hash       int h = k.threadLocalHashCode & (len - 1);       //hash地址不相等,就代表這個元素之前發生過hash沖突(本來應該放在這沒放在這),       //現在因為有元素被移除了,很有可能原來沖突的位置空出來了,重試一次       if (h != i) {         tab[i] = null;          //繼續采用鏈地址法存放元素         while (tab[h] != null)           h = nextIndex(h, len);         tab[h] = e;       }     }   }   return i; }

面試官:你有沒有用Threadlocal的工程實際經歷,給我講講。

安琪拉:有啊!

之前我跟你們一面面試官聊過,我是怎么把支付寶后臺負責的系統四十幾個核心rpc接口性能大幅度提升的,下面這個就是其中一個接口切流之后的效果,其中就用到了Threadlocal。圖片

面試官問到ThreadLocal的問題怎么回答

面試官:嗯,說說。

安琪拉:我剛才說有四十多個接口要做技改優化,那風險是很高的,我需要保證接口切換后業務不受影響,也叫等效切換。

流程是這樣的:

  • 把這四十多個接口按照業務含義定義了接口常量名稱,比如接口名alipay.quickquick.follow.angela;

  • 按照接口的流量從低到高開始切流,提前配置中心配置好每個接口的切流比例和用戶白名單;

  • 切流也有講究,先按照userId白名單切,再按照userId尾號切百分比,完全沒問題再完整切;

  • 在頂層抽象模版方法的入口通過ThreadLocal  Set 接口名,把接口名塞進去;

然后我在切流的地方通過ThreadLocal  獲取接口名,用于接口切流判斷切流;

面試官:最后一個問題,如果我有很多變量都要塞到ThreadlocalMap中,那豈不是要申明很多個Threadlocal  對象?有沒有好的解決辦法。

安琪拉:我們的最佳實踐是搞個再封裝一下,把ThreadLocalMap 的value  弄成Map就好了,這樣只要一個Threadlocal 對象就好了。

面試官:能詳細講講嗎?

安琪拉:講不動了,太累了。

面試官:講講。

安琪拉:真不想講了。

面試官:那今天先到這,您出了這個門右拐,回去等通知吧!

到此,相信大家對“面試官問到ThreadLocal的問題怎么回答”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女