細說ThreadLocal(一)

qzlzzz發表於2021-11-21

前言

java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域。如下圖所示:

其中堆是佔虛擬機器中記憶體最大的,堆被所有執行緒所共享,其最主要的便是存放例項物件。也因為堆記憶體是共享的,因此在多執行緒操作的條件下,多執行緒中堆記憶體中的資料十分容易發生執行緒安全的問題。因此為了保證多個執行緒對變數的安全訪問,我們可以將變數放到ThreadLocal物件中,變數在每個執行緒中都有獨立值,執行緒只能操作自己的變數,訪問不到其他執行緒中的變數。

ThreadLocal

ThreadLocal顧名思義便是執行緒本地變數的意思,在JAVA程式中每new一個ThreadLocal物件例項時,每個執行緒就會有一個隸屬於自己的變數,一個專屬於執行緒的變數,也因此該變數不會被其他的執行緒訪問到,以此來規避了執行緒安全的問題。

那麼ThreadLocal如何使得每個執行緒擁有自己獨有的本地值呢?

在JDK8的版本中,每一個執行緒都有一個屬於自己的ThreadLocalMap,ThreadLocalMap隨著Thread的建立而存在,隨著Thread的例項銷燬而銷燬。

        //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);
        }

而ThreadLocalMap即是儲存本地變數的關鍵之一,它首先以ThreadLocal例項和變數值作為Entry物件的構造引數來構造Entry物件,後以ThreadLocal例項進行雜湊計算hash,在雜湊函式計算後,每個ThreadLocal會均勻地、獨立地被分佈在Entry陣列中,也就是會得到自己在Entry陣列中的索引值,然後用此索引將構造出來的Entry物件放入到Entry陣列中。也由於每個執行緒都有自己的ThreadLocalMap,因此變數值是存放在專屬於自己執行緒的ThreadLocalMap中,這個ThreadLocalMap其他執行緒獲取不到,所以每個執行緒都有專屬於自己的變數值,在操作的時候也是對自己專屬的變數值進行操作。

從上圖我們也可以知道ThreadLocalMap其實是由ThreadLocal來進行管理的,兩者的關係密不可分。

我們在平時的操作中,大多是操作ThreadLocal的get(),set()方法,似乎ThreadLocalMap接觸的較少,但在接下來深入ThreadLocal的時候,我們會發現ThreadLocal這個類其實是基於ThreadLocalMap來完成的。

ThreadLocal的get方法

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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中get方法的原始碼,通過原始碼我們可以瞭解到get方法有以下步驟:

  1. 首先它會獲取當前佔有CPU時間片的執行緒的例項,然後通過當前執行緒的例項呼叫getMap()方法來獲取當前執行緒的ThreadLocalMap。
 //getMap()方法
 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
  1. 如果ThreadLocalMap不為空的話,就以自身ThreadLocal例項作為引數呼叫ThreadLocalMap的getEntry()方法來獲取到Entry物件,如果Entry物件不為空,則獲取Entry的value屬性值返回。
  2. 如果map為空的話或者此ThreadLocal例項計算出的hash值最為Entry陣列的索引在Entry陣列中並未存在Entry物件,證明當前執行緒並未初始化ThreadLocalMap,呼叫setInitialValue方法後返回。

ThreadLocal的setInitialValue方法

 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);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

我們來探尋get()方法中呼叫的setInitialValue方法,在以下程式碼中我們可以知道:

  1. 首先一上來就會呼叫一個鉤子函式initialValue()來給value變數賦值,但是我們進入initialValue()方法卻發現這個方法的返回值是null,如果需要繼承ThreadLocal來重寫這個方法就太麻煩,JDK已經為大家定義了ThreadLocal的內部SuppliedThreadLocal靜態子類,並且提供了ThreadLocal.withInitial()靜態工廠方法,我們只需要在定義一個ThreadLocal型別變數時,使用這個方法。

initialValue鉤子函式只會呼叫一次,且只在不使用ThreadLocal.set()方法去設定值就使用ThreadLocal.get()方法去獲取值的時候,會執行。

      //鉤子函式
      protected T initialValue() {
                return null;
          }

      //靜態工廠方法
      public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
          return new SuppliedThreadLocal<>(supplier);
      }
  1. get方法的步驟一致,也是獲取當前的執行緒後,獲取當前執行緒的ThreadLocalMap,如果沒有的話則建立為ThreadLocalMap建立一個map,這是因為一開始Thread下面的ThreadLocalMap初始值為空,所以有create這個步驟。在creatMap的方法中,我們可以看到了新建了一個ThreadLocalMap類,並以當前的ThreadLocal例項物件和initialValue產生的值作為構造引數,以此生成Entry物件儲存在Entry陣列中。
       void createMap(Thread t, T firstValue) {
          t.threadLocals = new ThreadLocalMap(this, firstValue);
       }
  1. 最後方法返回。

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);
        }
    }

以上是ThreadLocal的set方法的原始碼,它相對於get方法較簡單,也是獲取當前執行緒並獲取當前執行緒的ThreadLocalMap,如果有的話呼叫ThreadLocalMap的set方法將值設定進去,如果ThreadLocal為空,則使用createMap方法去建立一個ThreadLocalMap。

ThreadLocal的remove方法

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }

ThreadLocal的remove方法便更簡單了,它僅判斷獲取到的當前執行緒的ThreadLocalMap不為空,則呼叫了ThreadLocalMap的remove方法去刪除值。

後續

從以上的ThreadLocal函式,我們可以看到,許多重要的方法都是依靠著ThreadLocalMap及其api去完成對ThreadLocal方法的實現的,不難看出理解ThreadLocalMap其實相較於理解ThreadLocal是比較重要的,而ThreadLocalMap內部對Entry這個子類的實現,更是考慮到了ThreadLocal的記憶體洩漏,因此使用了WeakReference弱引用去關聯ThreadLocal例項,防止強引用導致的記憶體洩露的問題。

對ThreadLocalMap我會另開一個隨筆去寫,請多多擔待。

結尾

本文參考

[1] 周志明.深入理解Java虛擬機器:JVM高階特性與最佳實踐.-2版.北京:機械工業出版社,2013.6
[2] 尼恩.Java高併發程式設計.卷2,多執行緒、鎖、JMM、JUC、高併發設計模式.北京:機械工業出版社,2021,5

相關文章