<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試官再問你 ThreadLocal,就這樣狠狠 “懟” 回去!

          共 9100字,需瀏覽 19分鐘

           ·

          2021-03-30 12:34

          本文大綱

          1. 用過 ThreadLocal 嗎?在什么場(chǎng)景下會(huì)使用 ThreadLocal
          2. 講講 ThreadLocal 的原理吧!
          3. 使用 ThreadLocal 有什么需要注意的嗎?
          4. 有什么方式能提高 ThreadLocal 的性能嗎?
          5. 如何將 ThreadLocal 的數(shù)據(jù)傳遞到子線程中?
          6. 線程池中如何實(shí)現(xiàn) ThreadLocal 的數(shù)據(jù)傳遞?

          用過 ThreadLocal 嗎?在什么場(chǎng)景下會(huì)使用 ThreadLocal。

          這個(gè)回答一定要足夠自信:必須用過啊,無論是在平時(shí)的業(yè)務(wù)開發(fā)過程中會(huì)用到,其他很多三方框架中也都用到了 ThreadLocal。

          如果你回答沒用過,很有可能就涼涼了,因?yàn)?ThreadLocal 在很多場(chǎng)景都能用到,假如實(shí)在沒用過也不要沒信心,看完這篇文章你就知道如何回答了。

          場(chǎng)景一:ThreadLocal+MDC 實(shí)現(xiàn)鏈路日志增強(qiáng)

          日志增強(qiáng)之前也寫過一篇文章,講解了實(shí)現(xiàn)的功能,細(xì)節(jié)沒有講,可以看看下面這篇文章了解。

          文章:有了鏈路日志增強(qiáng),排查 Bug 小意思啦!

          比如我們需要在整個(gè)鏈路的日志中輸出當(dāng)前登錄的用戶 ID,首先就得在攔截器獲取過濾器中獲取用戶 ID,然后將用戶 ID 進(jìn)行存儲(chǔ)到 ThreadLocal。

          然后再層層進(jìn)行透?jìng)?,如果用?Dubbo,那么就在 Dubbo 的 Filter 中進(jìn)行傳遞到下一個(gè)服務(wù)中。問題來了,在 Dubbo 的 Filter 中如何獲取前面存儲(chǔ)的用戶 ID 呢?

          答案就是 ThreadLocal。獲取后添加到 MDC 中,就可以在日志中輸出用戶 ID。

          場(chǎng)景二:ThreadLocal 實(shí)現(xiàn)線程內(nèi)的緩存,避免重復(fù)調(diào)用

          緩存這塊就不重復(fù)講了,之前有單獨(dú)寫過文章,大家直接看之前的文章就可以了。

          文章:簡(jiǎn)直騷操作,ThreadLocal 還能當(dāng)緩存用

          場(chǎng)景三:ThreadLocal 實(shí)現(xiàn)數(shù)據(jù)庫讀寫分離下強(qiáng)制讀主庫

          首先你的項(xiàng)目中要做了讀寫分離,如果有對(duì)讀寫分離不了解的同學(xué)可以查看這篇文章:讀寫分離

          某些業(yè)務(wù)場(chǎng)景下,必須保證數(shù)據(jù)的及時(shí)性。主從同步有延遲,可以使用強(qiáng)制讀主庫來保證數(shù)據(jù)的一致性。

          在 Sharding JDBC 中,有提供對(duì)應(yīng)的 API 來設(shè)置強(qiáng)制路由到主庫,具體代碼如下:

          HintManager hintManager = HintManager.getInstance();
          hintManager.setMasterRouteOnly();

          HintManager 中就使用了 ThreadLocal 來存儲(chǔ)相關(guān)信息。這樣就可以實(shí)現(xiàn)在業(yè)務(wù)代碼中設(shè)置路由信息,在底層的數(shù)據(jù)庫路由那塊獲取信息,實(shí)現(xiàn)優(yōu)雅的數(shù)據(jù)傳遞。

          public final class HintManager implements AutoCloseable {
          private static final ThreadLocal<HintManager> HINT_MANAGER_HOLDER = new ThreadLocal();
          // ...............
          }

          場(chǎng)景四:ThreadLocal 實(shí)現(xiàn)同一線程下多個(gè)類之間的數(shù)據(jù)傳遞

          在 Spring Cloud Zuul 中,過濾器是必須要用的。用過濾器我們可以實(shí)現(xiàn)權(quán)限認(rèn)證,日志記錄,限流等功能。

          過濾器有多個(gè),而且是按順序執(zhí)行的。過濾器之前要透?jìng)鲾?shù)據(jù)該如何處理?

          Zuul 中已經(jīng)提供了 RequestContext 來實(shí)現(xiàn)數(shù)據(jù)傳遞,比如我們?cè)谶M(jìn)行攔截的時(shí)候會(huì)使用下面的代碼告訴負(fù)責(zé)轉(zhuǎn)發(fā)的過濾器不要進(jìn)行轉(zhuǎn)發(fā)操作。

          RequestContext.getCurrentContext().setSendZuulResponse(false);

          RibbonRoutingFilter 中就可以通過 RequestContext 獲取對(duì)應(yīng)的信息。

          @Override
          public boolean shouldFilter() {
          RequestContext ctx = RequestContext.getCurrentContext();
          return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
          && ctx.sendZuulResponse());
          }

          RequestContext 中就用了 ThreadLocal。

          public class RequestContext extends ConcurrentHashMap<String, Object> {
          protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
          @Override
          protected RequestContext initialValue() {
          try {
          return contextClass.newInstance();
          } catch (Throwable e) {
          throw new RuntimeException(e);
          }
          }
          };
          // .........................
          }

          講講 ThreadLocal 的原理吧!

          ThreadLocal 在使用的時(shí)候是單獨(dú)創(chuàng)建對(duì)象的,更像一個(gè)全局的容器。但是大家有沒有想過一個(gè)問題,就是為啥要設(shè)計(jì) ThreadLocal 這個(gè)類,而不使用 HashMap 這樣的容器類?

          ThreadLocal 本質(zhì)上是要解決線程之間數(shù)據(jù)的隔離,以達(dá)到互不影響的目的。如果我們用一個(gè) Map 做數(shù)據(jù)存儲(chǔ),Key 為線程 ID, Value 為你要存儲(chǔ)的內(nèi)容,其實(shí)也是能達(dá)到隔離的效果。

          沒錯(cuò),效果是能達(dá)到,但是性能就不一定好了,涉及到多個(gè)線程進(jìn)行數(shù)據(jù)操作。如果你不看 ThreadLocal 的源碼,你肯定也會(huì)以為 ThreadLocal 就是這么實(shí)現(xiàn)的。

          ThreadLocal 在設(shè)計(jì)這塊很巧妙,會(huì)在 Thread 類中嵌入一個(gè) ThreadLocalMap,ThreadLocalMap 就是一個(gè)容器,用于存儲(chǔ)數(shù)據(jù)的,但它在 Thread 類中,也就說存儲(chǔ)的就是這個(gè) Thread 類專享的數(shù)據(jù)。

          原本我們以為的 ThreadLocal 設(shè)置值的代碼:

          public void set(T value) {
          Thread t = Thread.currentThread();
          ThreadLocal.put(t.getId(), value);
          }

          正在的設(shè)置值的代碼:

          public void set(T value) {
                  Thread t = Thread.currentThread();
                  ThreadLocalMap map = getMap(t);
                  if (map != null)
                      map.set(this, value);
                  else
                      createMap(t, value);
              }

          可以看到,先是獲取當(dāng)前線程對(duì)象,然后從當(dāng)前線程中獲取線程的 ThreadLocalMap,值是添加到這個(gè) ThreadLocalMap 中的,key 就是當(dāng)前 ThreadLocal 的對(duì)象。從使用的 API 看上去像是把值存儲(chǔ)在了 ThreadLocal 中,其實(shí)值是存儲(chǔ)在線程內(nèi)部,然后關(guān)聯(lián)了對(duì)應(yīng)的 ThreadLocal,這樣通過 ThreadLocal.get 時(shí)就能獲取到對(duì)應(yīng)的值。

          public T get() {
          Thread t = Thread.currentThread();
          ThreadLocalMap map = getMap(t);
          if (map != null) {
          ThreadLocalMap.Entry e = map.getEntry(this);
          if (e != null) {
          @SuppressWarnings("unchecked")
          T result = (T)e.value;
          return result;
          }
          }
          return setInitialValue();
          }

          來張圖感受下:

          使用 ThreadLocal 有什么需要注意的嗎?

          • 避免跨線程異步傳遞,雖然有解決方案,文末介紹了方案
          • 使用時(shí)記得及時(shí) remove, 防止內(nèi)存泄露
          • 注釋說明使用場(chǎng)景,方便后人
          • 對(duì)性能有極致要求可以參考開源框架的做法,用一些優(yōu)化后的類,比如 FastThreadLocal

          有什么方式能提高 ThreadLocal 的性能嗎?

          這個(gè)問題其實(shí)是考察你對(duì)其他的一些框架的了解,因?yàn)樵谝恍╅_源的框架中也有使用 ThreadLocal 的場(chǎng)景,但是這些框架為了讓性能更好,一般都會(huì)做一些優(yōu)化。

          比如 Netty 中就重寫了一個(gè) FastThreadLocal 來代替 ThreadLocal,性能在一定場(chǎng)景下比 ThreadLocal 更好。

          性能提升主要表現(xiàn)在如下幾點(diǎn):

          • FastThreadLocal 操作數(shù)據(jù)的時(shí)候,會(huì)使用下標(biāo)的方式在數(shù)組中進(jìn)行查找來代替 ThreadLocal 通過哈希的方式進(jìn)行查找。
          • FastThreadLocal 利用字節(jié)填充來解決偽共享問題。

          其實(shí)除了 Netty 中對(duì) ThreadLocal 進(jìn)行了優(yōu)化,自定義了 FastThreadLocal。在其他的框架中也有類似的優(yōu)化,比如 Dubbo 中就 InternalThreadLocal,根據(jù)源碼中的注釋,也是參考了 FastThreadLocal 的設(shè)計(jì),基本上差不多。

          如何將 ThreadLocal 的數(shù)據(jù)傳遞到子線程中?

          InheritableThreadLocal 可以將值從當(dāng)前線程傳遞到子線程中,但這種場(chǎng)景其實(shí)用的不多,我相信很多人都沒怎么聽過 InheritableThreadLocal。

          那為什么 InheritableThreadLocal 就可以呢?

          InheritableThreadLocal 這個(gè)類繼承了 ThreadLocal,重寫了 3 個(gè)方法,在當(dāng)前線程上創(chuàng)建一個(gè)新的線程實(shí)例 Thread 時(shí),會(huì)把這些線程變量從當(dāng)前線程傳遞給新的線程實(shí)例。

          public class InheritableThreadLocal<T> extends ThreadLocal<T> {
              /**
               * Computes the child's initial value for this inheritable thread-local
               * variable as a function of the parent's value at the time the child
               * thread is created.  This method is called from within the parent
               * thread before the child is started.
               * <p>
               * This method merely returns its input argument, and should be overridden
               * if a different behavior is desired.
               *
               * @param parentValue the parent thread's value
               * @return the child thread's initial value
               */
              protected T childValue(T parentValue) {
                  return parentValue;
              }
              /**
               * Get the map associated with a ThreadLocal.
               *
               * @param t the current thread
               */
              ThreadLocalMap getMap(Thread t) {
                 return t.inheritableThreadLocals;
              }
              /**
               * Create the map associated with a ThreadLocal.
               *
               * @param t the current thread
               * @param firstValue value for the initial entry of the table.
               */
              void createMap(Thread t, T firstValue) {
                  t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
              }
          }

          通過上面的代碼我們可以看到 InheritableThreadLocal 重寫了 childValue, getMap,createMap 三個(gè)方法,當(dāng)我們往里面 set 值的時(shí)候,值保存到了 inheritableThreadLocals 里面,而不是之前的 threadLocals。

          關(guān)鍵的點(diǎn)來了,為什么當(dāng)創(chuàng)建新的線程時(shí),可以獲取到上個(gè)線程里的 threadLocal 中的值呢?原因就是在新創(chuàng)建線程的時(shí)候,會(huì)把之前線程的 inheritableThreadLocals 賦值給新線程的 inheritableThreadLocals,通過這種方式實(shí)現(xiàn)了數(shù)據(jù)的傳遞。

          源碼最開始在 Thread 的 init 方法中,如下:

           if (parent.inheritableThreadLocals != null)
               this.inheritableThreadLocals =
                          ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

          createInheritedMap 如下:

           static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
                  return new ThreadLocalMap(parentMap);
              }

          賦值代碼:

           private ThreadLocalMap(ThreadLocalMap parentMap) {
                Entry[] parentTable = parentMap.table;
                int len = parentTable.length;
                setThreshold(len);
                table = new Entry[len];
                for (int j = 0; j < len; j++) {
                      Entry e = parentTable[j];
                      if (e != null) {
                          @SuppressWarnings("unchecked")
                          ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                          if (key != null) {
                              Object value = key.childValue(e.value);
                              Entry c = new Entry(key, value);
                              int h = key.threadLocalHashCode & (len - 1);
                              while (table[h] != null)
                                  h = nextIndex(h, len);
                              table[h] = c;
                              size++;
                          }
                      }
                  }
          }

          線程池中如何實(shí)現(xiàn) ThreadLocal 的數(shù)據(jù)傳遞?

          如果涉及到線程池使用 ThreadLocal, 必然會(huì)出現(xiàn)問題。首先線程池的線程是復(fù)用的,其次,比如你從 Tomcat 的線程到自己的業(yè)務(wù)線程,也就是跨線程池了,線程也就不是之前的那個(gè)線程了,也就是說 ThreadLocal 就用不了,那么如何解決呢?

          可以使用阿里的 ttl 來解決,之前我也寫過一篇文章,可以查看:Spring Cloud 中 Hystrix 線程隔離導(dǎo)致 ThreadLocal 數(shù)據(jù)丟失

          貼上 ttl 的鏈接:https://github.com/alibaba/transmittable-thread-local

          ttl 是基于代碼方式的改造,下面再給大家介紹一種不用改造代碼的方式,基于 Java Agent 來實(shí)現(xiàn)的,牛的一批。

          鏈接:https://github.com/Nepxion/DiscoveryAgent

          關(guān)于作者:尹吉?dú)g,簡(jiǎn)單的技術(shù)愛好者,《Spring Cloud 微服務(wù)-全棧技術(shù)與案例解析》, 《Spring Cloud 微服務(wù) 入門 實(shí)戰(zhàn)與進(jìn)階》作者






          0、重磅!兩萬字長(zhǎng)文總結(jié),梳理 Java 入門進(jìn)階哪些事(推薦收藏)

          1、二月,拉開牛氣沖天的一年

          2、都退稅了嗎?和你聊聊發(fā)工資的騷操作。。

          3、想蛻變,就必須翻過算法這座山

          瀏覽 40
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色成人在线免费 | 抽插美女逼的视频 | 又黄又爽又粗又大又长又硬视屏 | 中文无码在线播放 | 一级高清片 |