<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基本解析

          共 24773字,需瀏覽 50分鐘

           ·

          2021-06-12 02:18

          點擊上方藍色字體,選擇“標星公眾號”

          優(yōu)質(zhì)文章,第一時間送達

          一 、基本了解

          ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,可能很多朋友都知道ThreadLocal為變量在每個線程中都創(chuàng)建了一個副本,那么每個線程可以訪問自己內(nèi)部的副本變量。

          1.1 作用

          • 線程并發(fā): 在多線程并發(fā)的場景下

          • 傳遞數(shù)據(jù): 我們可以通過ThreadLocal在同一線程,不同組件中傳遞公共變量

          • 線程隔離: 每個線程的變量都是獨立的,不會互相影響

          1.2 基本方法

          • public void set( T value) 設(shè)置當前線程綁定的局部變量

          • public T get() 獲取當前線程綁定的局部變量

          • public void remove() 移除當前線程綁定的局部變量

          • protected T initialValue() 初始化值

          1.3 入門案例

          需求:模擬多個線程設(shè)置值并取出該線程設(shè)置的值

          synchronized版

          package ThreadLocal;

          /**
           * @author shu
           * @date 2021/6/6
           * @description
           */
          public class ThreadLocalText {
              //變量
              private String variable;

              public String getVariable() {
                  return variable;
              }

              public void setVariable(String variable) {
                  this.variable = variable;
              }

              public static void main(String[] args) {
                  ThreadLocalText text = new ThreadLocalText();
                  for (int i = 0; i < 5; i++) {
                      new Thread(new Runnable() {
                          @Override
                          public void run() {
                              synchronized (ThreadLocalText.class) {
                                  text.setVariable(Thread.currentThread().getName() + "變量");
                                  System.out.println("<------------------------------------->");
                                  String variable = text.getVariable();
                                  System.out.println(Thread.currentThread().getName() + "--->" + variable);
                              }
                          }
                      }).start();
                  }
              }
          }


          ThreadLocal版

          package ThreadLocal;

          /**
           * @author shu
           * @date 2021/6/6
           * @description
           */
          public class ThreadLocalText {

              private static ThreadLocal<String> t1 = new ThreadLocal<>();
              //變量
              private String variable;


              public String getVariable() {
                  return t1.get();
              }

              public void setVariable(String variable) {
                   t1.set(variable);
              }

              public static void main(String[] args) {
                  ThreadLocalText text = new ThreadLocalText();
                  for (int i = 0; i < 5; i++) {
                      new Thread(new Runnable() {
                          @Override
                          public void run() {
                              //synchronized (ThreadLocalText.class) {
                                  text.setVariable(Thread.currentThread().getName() + "變量");
                                  String variable = text.getVariable();
                                  System.out.println(Thread.currentThread().getName() + "--->" + variable);
                              }
                         // }
                      }).start();
                  }
              }
          }


          比較

          以上兩種方法看似都可以解決問題,但是我們來考慮一下效率問題?

          • synchronized:同步機制采用以時間換空間的方式, 只提供了一份變量,讓不同的線程排隊訪問,側(cè)重于多個線程之間訪問資源的同步

          • ThreadLocalThreadLocal采用以空間換時間的方式, 為每一個線程都提供了一份變量的副本,從而實現(xiàn)同時訪問而相不干擾,側(cè)重于多線程中讓每個線程之間的數(shù)據(jù)相互隔離

          • 當然每個方法的適用場景不一樣

          二、 原理探究

          2.1 內(nèi)部結(jié)構(gòu)

          • JDK1.8之前:每個ThreadLocal都創(chuàng)建一個Map,然后用線程作為Mapkey,要存儲的局部變量作為Mapvalue,這樣就能達到各個線程的局部變量隔離的效果。

          • JDK1.8之后:每個Thread維護一個ThreadLocalMap,這個Map的keyThreadLocal實例本身,value才是真正要存儲的值Object。

          • 這樣設(shè)計之后每個Map存儲的Entry數(shù)量就會變少。因為之前的存儲數(shù)量由Thread的數(shù)量決定,現(xiàn)在是由ThreadLocal的數(shù)量決定。在實際運用當中,往往ThreadLocal的數(shù)量要少于Thread的數(shù)量。

          • 當Thread銷毀之后,對應(yīng)的ThreadLocalMap也會隨之銷毀,能減少內(nèi)存的使用。

          2.2 ThreadLocalMap源碼分析

          • 重要參數(shù)

              /**
               * 初始容量 —— 必須是2的整次冪
               */
              private static final int INITIAL_CAPACITY = 16;

              /**
               * 存放數(shù)據(jù)的table,Entry類的定義在下面分析
               * 同樣,數(shù)組長度必須是2的整次冪。
               */
              private Entry[] table;

              /**
               * 數(shù)組里面entrys的個數(shù),可以用于判斷table當前使用量是否超過閾值。
               */
              private int size = 0;

              /**
               * 進行擴容的閾值,表使用量大于它的時候進行擴容。
               */
              private int threshold; // Default to 0


          • 存儲結(jié)構(gòu) - Entry

          //規(guī)定死是弱引用 WeakReference
          static class Entry extends WeakReference<ThreadLocal<?>> {
              /** The value associated with this ThreadLocal. */
              Object value;

              Entry(ThreadLocal<?> k, Object v) {
                  super(k);
                  value = v;
              }
          }

          • hash沖突的解決

          Hash沖突基本知識

          哈希是通過對數(shù)據(jù)進行再壓縮,提高效率的一種解決方法。但由于通過哈希函數(shù)產(chǎn)生的哈希值是有限的,而數(shù)據(jù)可能比較多,導致經(jīng)過哈希函數(shù)處理后仍然有不同的數(shù)據(jù)對應(yīng)相同的值。這時候就產(chǎn)生了哈希沖突。

          • 開放地址方法

            (1)線性探測

            按順序決定值時,如果某數(shù)據(jù)的值已經(jīng)存在,則在原來值的基礎(chǔ)上往后加一個單位,直至不發(fā)生哈希沖突。

            (2)再平方探測

            按順序決定值時,如果某數(shù)據(jù)的值已經(jīng)存在,則在原來值的基礎(chǔ)上先加1的平方個單位,若仍然存在則減1的平方個單位。隨之是2的平方,3的平方等等。直至不發(fā)生哈希沖突。

            (3)偽隨機探測

            按順序決定值時,如果某數(shù)據(jù)已經(jīng)存在,通過隨機函數(shù)隨機生成一個數(shù),在原來值的基礎(chǔ)上加上隨機數(shù),直至不發(fā)生哈希沖突。

          • 鏈式地址法(HashMap的哈希沖突解決方法)

            對于相同的值,使用鏈表進行連接。使用數(shù)組存儲每一個鏈表。

            優(yōu)點:

            (1)拉鏈法處理沖突簡單,且無堆積現(xiàn)象,即非同義詞決不會發(fā)生沖突,因此平均查找長度較短;

            (2)由于拉鏈法中各鏈表上的結(jié)點空間是動態(tài)申請的,故它更適合于造表前無法確定表長的情況;

            (3)開放定址法為減少沖突,要求裝填因子α較小,故當結(jié)點規(guī)模較大時會浪費很多空間。而拉鏈法中可取α≥1,且結(jié)點較大時,拉鏈法中增加的指針域可忽略不計,因此節(jié)省空間;

            (4)在用拉鏈法構(gòu)造的散列表中,刪除結(jié)點的操作易于實現(xiàn)。只要簡單地刪去鏈表上相應(yīng)的結(jié)點即可。
              缺點:

            指針占用較大空間時,會造成空間浪費,若空間用于增大散列表規(guī)模進而提高開放地址法的效率。

          • 建立公共溢出區(qū)

            建立公共溢出區(qū)存儲所有哈希沖突的數(shù)據(jù)。

          • 再哈希法

            對于沖突的哈希值再次進行哈希處理,直至沒有哈希沖突。


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


            ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
             //初始化table
                      table = new Entry[INITIAL_CAPACITY];
                      //計算插入值的地址
                      int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                      //設(shè)置值
                      table[i] = new Entry(firstKey, firstValue);
                      size = 1;
                      //設(shè)置閾值
                      setThreshold(INITIAL_CAPACITY);
                  }


          //計算索引值 threadLocalHashCode
            private final int threadLocalHashCode = nextHashCode();
              
              private static int nextHashCode() {
                  return nextHashCode.getAndAdd(HASH_INCREMENT);
              }
          //AtomicInteger是一個提供原子操作的Integer類,通過線程安全的方式操作加減,適合高并發(fā)情況下的使用
              private static AtomicInteger nextHashCode =  new AtomicInteger();
               //特殊的hash
              private static final int HASH_INCREMENT = 0x61c88647;
            
          // INITIAL_CAPACITY - 1
          private void set(ThreadLocal<?> key, Object value) {
                  ThreadLocal.ThreadLocalMap.Entry[] tab = table;
                  int len = tab.length;
                  //計算索引(重點代碼,剛才分析過了)
                  int i = key.threadLocalHashCode & (len-1);
                  /**
                   * 使用線性探測法查找元素(重點代碼)
                   */
                  for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
                       e != null;
                       e = tab[i = nextIndex(i, len)]) {
                      ThreadLocal<?> k = e.get();
                      //ThreadLocal 對應(yīng)的 key 存在,直接覆蓋之前的值
                      if (k == key) {
                          e.value = value;
                          return;
                      }
                      // key為 null,但是值不為 null,說明之前的 ThreadLocal 對象已經(jīng)被回收了,
                     // 當前數(shù)組中的 Entry 是一個陳舊(stale)的元素
                      if (k == null) {
                          //用新元素替換陳舊的元素,這個方法進行了不少的垃圾清理動作,防止內(nèi)存泄漏
                          replaceStaleEntry(key, value, i);
                          return;
                      }
                  }
              
               //ThreadLocal對應(yīng)的key不存在并且沒有找到陳舊的元素,則在空元素的位置創(chuàng)建一個新的Entry。
                      tab[i] = new Entry(key, value);
                      int sz = ++size;
                      /**
                       * cleanSomeSlots用于清除那些e.get()==null的元素,
                       * 這種數(shù)據(jù)key關(guān)聯(lián)的對象已經(jīng)被回收,所以這個Entry(table[index])可以被置null。
                       * 如果沒有清除任何entry,并且當前使用量達到了負載因子所定義(長度的2/3),那么進行     * rehash(執(zhí)行一次全表的掃描清理工作)
                       */
                      if (!cleanSomeSlots(i, sz) && sz >= threshold)
                          rehash();
          }

           /**
               * 獲取環(huán)形數(shù)組的下一個索引
               */
              private static int nextIndex(int i, int len) {
                  return ((i + 1 < len) ? i + 1 : 0);
              }


          總結(jié)

          • 計算hash的時候里面采用了hashCode & (size - 1)的算法,這相當于取模運算hashCode % size的一個更高效的實現(xiàn)。正是因為這種算法,我們要求size必須是2的整次冪,這也能保證在索引不越界的前提下,使得hash發(fā)生沖突的次數(shù)減小。

          • 線性探測法:假設(shè)當前table長度為16,也就是說如果計算出來key的hash值為14,如果table[14]上已經(jīng)有值,并且其key與當前key不一致,那么就發(fā)生了hash沖突,這個時候?qū)?4加1得到15,取table[15]進行判斷,這個時候如果還是沖突會回到0,取table[0],以此類推,直到可以插入。

          2.3基本方法詳解

          • public void set( T value) 設(shè)置當前線程綁定的局部變量

          • public T get() 獲取當前線程綁定的局部變量

          • public void remove() 移除當前線程綁定的局部變量

          • protected T initialValue() 初始化值

          set方法

            /**
               * 設(shè)置當前線程對應(yīng)的ThreadLocal的值
               *
               * @param value 將要保存在當前線程對應(yīng)的ThreadLocal的值
               */
              public void set(T value) {
                  // 獲取當前線程對象
                  Thread t = Thread.currentThread();
                  // 獲取此線程對象中維護的ThreadLocalMap對象
                  ThreadLocalMap map = getMap(t);
                  // 判斷map是否存在
                  if (map != null)
                      // 存在則調(diào)用map.set設(shè)置此實體entry
                      map.set(this, value);
                  else
                      // 1)當前線程Thread 不存在ThreadLocalMap對象
                      // 2)則調(diào)用createMap進行ThreadLocalMap對象的初始化
                      // 3)并將 t(當前線程)和value(t對應(yīng)的值)作為第一個entry存放至ThreadLocalMap中
                      createMap(t, value);
              }

           /**
               * 獲取當前線程Thread對應(yīng)維護的ThreadLocalMap 
               * 
               * @param  t the current thread 當前線程
               * @return the map 對應(yīng)維護的ThreadLocalMap 
               */
              ThreadLocalMap getMap(Thread t) {
                  return t.threadLocals;
              }
           /**
               *創(chuàng)建當前線程Thread對應(yīng)維護的ThreadLocalMap 
               *
               * @param t 當前線程
               * @param firstValue 存放到map中第一個entry的值
               */
           void createMap(Thread t, T firstValue) {
                  //這里的this是調(diào)用此方法的threadLocal
                  t.threadLocals = new ThreadLocalMap(this, firstValue);
              }


          get方法

              /**
               * 返回當前線程中保存ThreadLocal的值
               * 如果當前線程沒有此ThreadLocal變量,
               * 則它會通過調(diào)用{@link #initialValue} 方法進行初始化值
               *
               * @return 返回當前線程對應(yīng)此ThreadLocal的值
               */
              public T get() {
                  // 獲取當前線程對象
                  Thread t = Thread.currentThread();
                  // 獲取此線程對象中維護的ThreadLocalMap對象
                  ThreadLocalMap map = getMap(t);
                  // 如果此map存在
                  if (map != null) {
                      // 以當前的ThreadLocal 為 key,調(diào)用getEntry獲取對應(yīng)的存儲實體e
                      ThreadLocalMap.Entry e = map.getEntry(this);
                      // 對e進行判空 
                      if (e != null) {
                          @SuppressWarnings("unchecked")
                          // 獲取存儲實體 e 對應(yīng)的 value值
                          // 即為我們想要的當前線程對應(yīng)此ThreadLocal的值
                          T result = (T)e.value;
                          return result;
                      }
                  }
                  /*
                   初始化 : 有兩種情況有執(zhí)行當前代碼
                   第一種情況: map不存在,表示此線程沒有維護的ThreadLocalMap對象
                   第二種情況: map存在, 但是沒有與當前ThreadLocal關(guān)聯(lián)的entry
                   */
                  return setInitialValue();
              }

              /**
               * 初始化
               *
               * @return the initial value 初始化后的值
               */
              private T setInitialValue() {
                  // 調(diào)用initialValue獲取初始化的值
                  // 此方法可以被子類重寫, 如果不重寫默認返回null
                  T value = initialValue();
                  // 獲取當前線程對象
                  Thread t = Thread.currentThread();
                  // 獲取此線程對象中維護的ThreadLocalMap對象
                  ThreadLocalMap map = getMap(t);
                  // 判斷map是否存在
                  if (map != null)
                      // 存在則調(diào)用map.set設(shè)置此實體entry
                      map.set(this, value);
                  else
                      // 1)當前線程Thread 不存在ThreadLocalMap對象
                      // 2)則調(diào)用createMap進行ThreadLocalMap對象的初始化
                      // 3)并將 t(當前線程)和value(t對應(yīng)的值)作為第一個entry存放至ThreadLocalMap中
                      createMap(t, value);
                  // 返回設(shè)置的值value
                  return value;
              }


          remove方法

           /**
               * 刪除當前線程中保存的ThreadLocal對應(yīng)的實體entry
               */
               public void remove() {
                  // 獲取當前線程對象中維護的ThreadLocalMap對象
                   ThreadLocalMap m = getMap(Thread.currentThread());
                  // 如果此map存在
                   if (m != null)
                      // 存在則調(diào)用map.remove
                      // 以當前ThreadLocal為key刪除對應(yīng)的實體entry
                       m.remove(this);
               }


          initialValue方法

          /**
            * 返回當前線程對應(yīng)的ThreadLocal的初始值
            
            * 此方法的第一次調(diào)用發(fā)生在,當線程通過get方法訪問此線程的ThreadLocal值時
            * 除非線程先調(diào)用了set方法,在這種情況下,initialValue 才不會被這個線程調(diào)用。
            * 通常情況下,每個線程最多調(diào)用一次這個方法。
            *
            * <p>這個方法僅僅簡單的返回null {@code null};
            * 如果程序員想ThreadLocal線程局部變量有一個除null以外的初始值,
            * 必須通過子類繼承{@code ThreadLocal} 的方式去重寫此方法
            * 通常, 可以通過匿名內(nèi)部類的方式實現(xiàn)
            *
            * @return 當前ThreadLocal的初始值
            */
          protected T initialValue() {
              return null;
          }


          三 、面試

          3.1 為什么ThreadLocalMap的key要設(shè)計成弱引用?

          • 內(nèi)存泄漏相關(guān)概念

            • Memory overflow:內(nèi)存溢出,沒有足夠的內(nèi)存提供申請者使用。

            • Memory leak: 內(nèi)存泄漏是指程序中已動態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費,導致程序運行速度減慢甚至系統(tǒng)崩潰等嚴重后果。內(nèi)存泄漏的堆積終將導致內(nèi)存溢出。

          • 引用相關(guān)概念

            • 強引用 只要引用存在,垃圾回收器永遠不會回收

            • 軟引用 非必須引用,內(nèi)存溢出之前進行回收

            • 弱引用 第二次垃圾回收時回收

            • 虛引用 垃圾回收時回收,無法通過引用取到對象值

          • 弱引用比強引用可以多一層保障:弱引用的ThreadLocal會被回收,對應(yīng)的value在下一次ThreadLocalMap調(diào)用set,get,remove中的任一方法的時候會被清除,從而避免內(nèi)存泄漏。






          瀏覽 37
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  日韩欧美在线观看视频 | 日韩一级片在线看 | 搭讪人妻中文字幕 | 俺来了,俺去了成人影视网 | 天天操天天看 |