僅用一個ThreadLocal,就把面試官說服的明明白白!

開場

杭州某商務樓裏,正發生着一塊兒求職者和麪試官的battle。java

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

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

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

安琪拉:精通。後端

對。。。,你沒看錯,問就是「精通」,把666打在評論區。數組

面試官服務器

僅用一個ThreadLocal,就把面試官說服的明明白白!

心想 莫不是個憨批,上來就說本身精通,誰把精通掛嘴上,莫不是個愣頭青嘞!數據結構

面試官:那咱們開始吧。用過Threadlocal 吧?多線程

安琪拉:用過。併發

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

安琪拉:咱們項目屬於保密項目,無可奉告,你仍是換個問題吧!

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

正題

安琪拉:show time。。。

僅用一個ThreadLocal,就把面試官說服的明明白白!

安琪拉:舉個栗子,咱們支付寶每秒鐘同時會有不少用戶請求,那每一個請求都帶有用戶信息,咱們知道一般都是一個線程處理一個用戶請求,咱們能夠把用戶信息丟到Threadlocal裏面,讓每一個線程處理本身的用戶信息,線程之間互不干擾。

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

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

僅用一個ThreadLocal,就把面試官說服的明明白白!

面試官:那你來給我講講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<UserInfo> userInfoThreadLocal = new ThreadLocal() 初始化了一個Threadlocal 對象,就是上圖中說的Threadlocal 引用,這個引用指向堆中的ThreadLocal 對象;
  • 而後咱們調用userInfoThreadLocal.set(userInfo); 這裏作了什麼事呢?

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

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

僅用一個ThreadLocal,就把面試官說服的明明白白!

僅用一個ThreadLocal,就把面試官說服的明明白白!

這裏是在當前線程對象的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,就把面試官說服的明明白白!

HashMap詳解:

參考
安琪拉,公衆號:安琪拉的博客一個HashMap跟面試官扯了半個小時

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 對象就行了。

面試官:能詳細講講嗎?

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

面試官:講講。

安琪拉:真不想講了。

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

原文連接: https://mp.weixin.qq.com/s/JUb2GR4CmokO0SklFeNmwg

相關文章
相關標籤/搜索