面試再問ThreadLocal,別說你不會

  • 2019 年 11 月 27 日
  • 筆記

作者:堅持就是勝利 juejin.im/post/5d427f306fb9a06b122f1b94

ThreadLocal是什麼

以前面試的時候問到ThreadLocal總是一臉懵逼,只知道有這個哥們,不了解他是用來做什麼的,更不清楚他的原理了。表面上看他是和多執行緒,執行緒同步有關的一個工具類,但其實他與執行緒同步機制無關。

執行緒同步機制是多個執行緒共享同一個變數,而ThreadLocal是為每個執行緒創建一個單獨的變數副本,每個執行緒都可以改變自己的變數副本而不影響其它執行緒所對應的副本。

官方API上是這樣介紹的:

該類提供了執行緒局部(thread-local)變數。這些變數不同於它們的普通對應物,因為訪問某個變數(通過其 get 或 set 方法)的每個執行緒都有自己的局部變數,它獨立於變數的初始化副本。ThreadLocal實例通常是類中的 private static 欄位,它們希望將狀態與某一個執行緒(例如,用戶 ID 或事務 ID)相關聯。

ThreadLocal的API

ThreadLocal定義了四個方法:

  • get():返回此執行緒局部變數當前副本中的值
  • set(T value):將執行緒局部變數當前副本中的值設置為指定值
  • initialValue():返回此執行緒局部變數當前副本中的初始值
  • remove():移除此執行緒局部變數當前副本中的值

ThreadLocal還有一個特別重要的靜態內部類ThreadLocalMap,該類才是實現執行緒隔離機制的關鍵。get()、set()、remove()都是基於該內部類進行操作,ThreadLocalMap用鍵值對方式存儲每個執行緒變數的副本,key為當前的ThreadLocal對象,value為對應執行緒的變數副本。

試想,每個執行緒都有自己的ThreadLocal對象,也就是都有自己的ThreadLocalMap,對自己的ThreadLocalMap操作,當然是互不影響的了,這就不存在執行緒安全問題了,所以ThreadLocal是以空間來交換安全性的解決思路。

使用實例

假設每個執行緒都需要一個計數值記錄自己做某件事做了多少次,各執行緒運行時都需要改變自己的計數值而且相互不影響,那麼ThreadLocal就是很好的選擇,這裡ThreadLocal里保存的當前執行緒的局部變數的副本就是這個計數值。

public class SeqCount {        private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>() {          @Override          protected Integer initialValue() {              return 0;          }      };          public int nextSeq() {          seqCount.set(seqCount.get() +1);          return seqCount.get();      }        public static void main(String [] args) {          SeqCount seqCount = new SeqCount();            SeqThread seqThread1 = new SeqThread(seqCount);          SeqThread seqThread2 = new SeqThread(seqCount);          SeqThread seqThread3 = new SeqThread(seqCount);          SeqThread seqThread4 = new SeqThread(seqCount);            seqThread1.start();          seqThread2.start();          seqThread3.start();          seqThread4.start();      }        public static class SeqThread extends Thread {            private SeqCount seqCount;            public SeqThread(SeqCount seqCount) {              this.seqCount = seqCount;          }            @Override          public void run() {              for (int i=0; i<3; i++) {                  System.out.println(Thread.currentThread().getName()+" seqCount:"+seqCount.nextSeq());              }          }      }   }

運行結果:

解決SimpleDateFormat的執行緒安全

我們知道SimpleDateFormat在多執行緒下是存在執行緒安全問題的,那麼將SimpleDateFormat作為每個執行緒的局部變數的副本就是每個執行緒都擁有自己的SimpleDateFormat,就不存在執行緒安全問題了。

public class SimpleDateFormatDemo {        private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";        private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<>();        /**       * 獲取執行緒的變數副本,如果不覆蓋initialValue方法,第一次get將返回null,故需要創建一個DateFormat,放入threadLocal中       * @return       */      public DateFormat getDateFormat() {          DateFormat df = threadLocal.get();          if (df == null) {              df = new SimpleDateFormat(DATE_FORMAT);              threadLocal.set(df);          }          return df;      }        public static void main(String [] args) {          SimpleDateFormatDemo formatDemo = new SimpleDateFormatDemo();            MyRunnable myRunnable1 = new MyRunnable(formatDemo);          MyRunnable myRunnable2 = new MyRunnable(formatDemo);          MyRunnable myRunnable3 = new MyRunnable(formatDemo);            Thread thread1= new Thread(myRunnable1);          Thread thread2= new Thread(myRunnable2);          Thread thread3= new Thread(myRunnable3);          thread1.start();          thread2.start();          thread3.start();      }          public static class MyRunnable implements Runnable {            private SimpleDateFormatDemo dateFormatDemo;            public MyRunnable(SimpleDateFormatDemo dateFormatDemo) {              this.dateFormatDemo = dateFormatDemo;          }            @Override          public void run() {              System.out.println(Thread.currentThread().getName()+" 當前時間:"+dateFormatDemo.getDateFormat().format(new Date()));          }      }  }

運行結果:

源碼分析

ThreadLocalMap

ThreadLocalMap內部是利用Entry來進行key-value的存儲的。

static class Entry extends WeakReference<ThreadLocal<?>> {              /** The value associated with this ThreadLocal. */              Object value;                Entry(ThreadLocal<?> k, Object v) {                  super(k);                  value = v;              }          }

上面源碼中key就是ThreadLocal,value就是值,Entry繼承WeakReference,所以Entry對應key的引用(ThreadLocal實例)是一個弱引用。

set(ThreadLocal key, Object value)

 /**           * Set the value associated with key.           *           * @param key the thread local object           * @param value the value to be set           */          private void set(ThreadLocal<?> key, Object value) {              Entry[] tab = table;              int len = tab.length;              //根據ThreadLocal的散列值,查找對應元素在數組中的位置              int i = key.threadLocalHashCode & (len-1);              //採用線性探測法尋找合適位置              for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {                  ThreadLocal<?> k = e.get();                  //key存在,直接覆蓋                  if (k == key) {                      e.value = value;                      return;                  }                  // key == null,但是存在值(因為此處的e != null),說明之前的ThreadLocal對象已經被回收了                  if (k == null) {                      replaceStaleEntry(key, value, i);                      return;                  }              }              //ThreadLocal對應的key實例不存在,new一個              tab[i] = new Entry(key, value);              int sz = ++size;              //清楚陳舊的Entry(key == null的)              // 如果沒有清理陳舊的 Entry 並且數組中的元素大於了閾值,則進行 rehash              if (!cleanSomeSlots(i, sz) && sz >= threshold)                  rehash();          }

這個set操作和集合Map解決散列衝突的方法不同,集合Map採用的是鏈地址法,這裡採用的是開放定址法(線性探測)。set()方法中的replaceStaleEntry()和cleanSomeSlots(),這兩個方法可以清除掉key ==null的實例,防止記憶體泄漏。

getEntry()

private Entry getEntry(ThreadLocal<?> key) {              int i = key.threadLocalHashCode & (table.length - 1);              Entry e = table[i];              if (e != null && e.get() == key)                  return e;              else                  return getEntryAfterMiss(key, i, e);          }

由於採用了開放定址法,當前keu的散列值和元素在數組中的索引並不是一一對應的,首先取一個猜測數(key的散列值),如果所對應的key是我們要找的元素,那麼直接返回,否則調用getEntryAfterMiss

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {              Entry[] tab = table;              int len = tab.length;                while (e != null) {                  ThreadLocal<?> k = e.get();                  if (k == key)                      return e;                  if (k == null)                      expungeStaleEntry(i);                  else                      i = nextIndex(i, len);                  e = tab[i];              }              return null;          }

這裡一直在探測尋找下一個元素,知道找的元素的key是我們要找的。這裡當key==null時,調用expungeStaleEntry有利於GC的回收,用於防止記憶體泄漏。

ThreadLocal為什麼會記憶體泄漏

ThreadLocalMap的key為ThreadLocal實例,他是一個弱引用,我們知道弱引用有利於GC的回收,當key == null時,GC就會回收這部分空間,但value不一定能被回收,因為他和Current Thread之間還存在一個強引用的關係。

由於這個強引用的關係,會導致value無法回收,如果執行緒對象不消除這個強引用的關係,就可能會出現OOM。有些時候,我們調用ThreadLocalMap的remove()方法進行顯式處理。

總結

ThreadLocal不是用來解決共享變數的問題,也不是協調執行緒同步,他是為了方便各執行緒管理自己的狀態而引用的一個機制。

每個ThreadLocal內部都有一個ThreadLocalMap,他保存的key是ThreadLocal的實例,他的值是當前執行緒的局部變數的副本的值。

推薦閱讀:

再談ThreadLocal

ThreadLocal造成記憶體溢出OOM