聊一聊執行緒變數綁定之ThreadLocal

  • 2019 年 12 月 19 日
  • 筆記

當使用 ThreadLocal 維護變數的時候 為每一個使用該變數的執行緒提供一個獨立的變數副本,即每個執行緒內部都會有一個該變數,這樣同時多個執行緒訪問該變數並不會彼此相互影響,因此他們使用的都是自己從記憶體中拷貝過來的變數的副本, 這樣就不存在執行緒安全問題,也不會影響程式的執行性能。但是要注意,雖然 ThreadLocal 能夠解決上面說的問題,但是由於在每個執行緒中都創建了副本,所以要考慮它對資源的消耗,比如記憶體的佔用會比不使用 ThreadLocal 要大。

示例

@Test      public void testSimpleThreadLocal() throws InterruptedException {          ThreadLocal threadLocal = new ThreadLocal();          threadLocal.set("simple thread local in main thread!");          Thread thread = new Thread(new Runnable() {              @Override              public void run() {                  System.out.println("inner thread:" + threadLocal.get());                  threadLocal.set("simple ThreadLocal in thread!");                  System.out.println("inner thread:" + threadLocal.get());              }          });          thread.start();          thread.join();          System.out.println(threadLocal.get());      }

輸出結果為:

inner thread:null  inner thread:simple ThreadLocal in thread!  simple thread local in main thread!
  • 可以看到,在 thread1 中可以通過 threadLocal 來進行變數的保存,在整個執行緒的上下文中都可以獲取到這個變數的值。就是為每一個使用該變數的執行緒都提供一個變數值的副本,每一個執行緒都可以獨立地改變自己的副本,而不會和其它執行緒的副本衝突。main 執行緒和 thread1 執行緒之間互不影響。
  • 另一方面,它也有一定的局限性,thread1 執行緒是 main 執行緒的子執行緒,但是父執行緒中的 threadLocal 變數與子執行緒是沒有達到共享的效果的。當然,在它的子類 InheritableThreadLocal 實現了這個特性。在聊 InheritableThreadLocal 時會進一步講解。

原理

這裡我們從源碼角度來聊一聊 ThreadLocal 的原理。先來看一看它的屬性和方法:

我們先來走一遍流程,然後再回過頭來看一看每個方法的作用。

set 方法

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

先看下 getMap 方法:

ThreadLocalMap getMap(Thread t) {      return t.threadLocals;   }

要了解這個,我們需要先看一看 Thread 類中的兩個屬性:

/* ThreadLocal values pertaining to this thread. This map is maintained       * by the ThreadLocal class. */      ThreadLocal.ThreadLocalMap threadLocals = null;        /*       * InheritableThreadLocal values pertaining to this thread. This map is       * maintained by the InheritableThreadLocal class.       */      ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

這兩個參數在 new Thread 時會進行初始化:

public Thread() {          init(null, null, "Thread-" + nextThreadNum(), 0);      }       private void init(ThreadGroup g, Runnable target, String name,                        long stackSize, AccessControlContext acc) {      .............       if (parent.inheritableThreadLocals != null)              this.inheritableThreadLocals =                  ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);      ............      }

需要關注的是在初始化時會將父執行緒的 inheritableThreadLocals 賦給子執行緒(這點在後面講到 inheritableThreadLocals 時特別有用)。

我們再回過來看 set 方法,在 getMap 時第一次獲取到的值為 null,繼而會執行 createMap 方法:

void createMap(Thread t, T firstValue) {          t.threadLocals = new ThreadLocalMap(this, firstValue);      }

會給當前執行緒的 threadLocals 變數賦一個新創建的 ThreadLocalMap 對象。傳入的是 threadLocal 變數和要 set 的值,我們看一下 ThreadLocalMap 的構造方法:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {          table = new Entry[INITIAL_CAPACITY];          int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);          table[i] = new Entry(firstKey, firstValue);          size = 1;          setThreshold(INITIAL_CAPACITY);      }

關於這個構造我們來聊以下幾點:

  • INITIAL_CAPACITY 初始值是 16,是 2 的指數倍。取這個值是因為在 2 的指數倍情況下 hash 值是完全散列的(無 hash 衝突)。
  • table 為 Entry 類型的數組。
  • firstKey.threadLocalHashCode 的值是一直增加的不會重複的值,用於取 table 的下標來用的。它的初始值具說和斐波那契數列(和黃金分割數)有關。(根號 5-1)*2 的 31 次方,轉換成 long 類型就是 2654435769,轉換成 int 類型就是-1640531527(這點說明來自:cnblogs.com/dennyzhangdd/p/7978455.html)
  • Entry extends WeakReference<threadlocal</threadlocal

這裡是不是想到了 hashmap 中的用法——用 key 的 hashcode 對容量減一取&操作的結果應該是該值在 map 中的索引位置。前提是容量是 2 的指數倍。

而當第二次調用 ThreadLocal 的 set(value)方法時,會直接進入下面這個方法:

private void set(ThreadLocal<?> key, Object value) {        // We don't use a fast path as with get() because it is at      // least as common to use set() to create new entries as      // it is to replace existing ones, in which case, a fast      // path would fail more often than not.        Entry[] tab = table;      int len = tab.length;      int i = key.threadLocalHashCode & (len-1);      //相當於i < len - 1的一個遍歷      for (Entry e = tab[i];              e != null;              e = tab[i = nextIndex(i, len)]) {          //e的值是tab[i]位置上的那個Entry          ThreadLocal<?> k = e.get();          //條件一:如果這個ThreadLocal的引用已經存在時,則直接更改Entry的value值。          if (k == key) {              e.value = value;              return;          }          //條件二:e!=null成立,證明這個Entry之前存在,但是現在這個Entry中的key為空時          if (k == null) {              //如果key為null,用新key、value覆蓋,同時清理歷史key=null的陳舊數據              //在replaceStaleEntry中會從i位置開始向前和向後清理table上的key為null的Entry,減小空間佔用              replaceStaleEntry(key, value, i);              return;          }      }      //如果不符合上述兩個條件時,直接放入位置i      tab[i] = new Entry(key, value);      int sz = ++size;      //1. 清理一些slot      //2. 是否需要擴容,如果需要擴容則需要將老的數據進行rehash重新放入容器中      if (!cleanSomeSlots(i, sz) && sz >= threshold)          rehash();  }       private static int nextIndex(int i, int len) {      return ((i + 1 < len) ? i + 1 : 0);  }

這裡需要注意一點是,當兩個 hash 衝突時會有覆蓋發生。

get 方法

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();  }    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);      }    private T setInitialValue() {          T value = initialValue();          Thread t = Thread.currentThread();          ThreadLocalMap map = getMap(t);          if (map != null)              map.set(this, value);          else              createMap(t, value);          return value;      }

對於 get 方法我們做以下幾點講解:

  • getMap 方法與上面的 set 方法同解。
  • 如果沒有 set 過值,也就是 map 為 null 時會調用 setInitialValue 方法,這個方法會調用 initialValue 方法,這個方法在後面的其他兩種 ThreadLocal 實現中你會越來越熟悉的。當然,在這裡初始的值為 null。
  • getEntry 方法也十分地簡單,通過 ThreadLocal 實例中的 threadLocalHashCode 值定位到它所在的 Entry 在 table 中的下標 i 的值,然後從 table 中取出 Entry,然後通過 ThreadLocal 實例取值即可。

其他方法

  • remove 方法是用來清除 table 中的某個 entry 的。
  • childValue 在這裡是一個模板方法,在另外兩種實現中都有用到。
  • createInheritedMap 會在 Thread 的構造方法中使用到,上面已經提到過,之後在 inheritableThreadLocals 還會亮相。

關於 ThreadLocal 的部分就聊到這裡,通過上面的流程我們可以看出,ThreadLocal 是用來隔離每個執行緒的變數使用的,對於父子執行緒的變數傳遞卻並不適合,那麼怎麼拿到父執行緒的共享變數值呢,下節的 inheritableThreadLocals 會告訴你答案。