【JavaSE】淺談TreadLocal,TreadLocal的常用方法set()、get()、remove()原始碼分析

馮某r發表於2019-01-21

1.TreadLocal是什麼

先來看一段程式碼:

public class Test {
    private static String commStr;
    private static ThreadLocal<String> threadStr = new ThreadLocal<String>();
    public static void main(String[] args) {
        commStr = "main";
        threadStr.set("main");
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                commStr = "thread";
                threadStr.set("thread");
            }
        });
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(commStr);    //thread
        System.out.println(threadStr.get());   //main
    }
}

在主類中建立了兩個靜態變數,一個String,另一個TreadLocal類建立的物件,雖然都是靜態的,但是輸出結果卻不是兩個thread(正確輸出結果在輸出語句後面註釋),說明普通的靜態變數,主執行緒和各個執行緒對它產生的改變都可以對其他執行緒產生影響。而TreadLocal類建立的物件對每個執行緒來說是獨立的,為每個執行緒建立了一個副本變數。用這種方法可以解決執行緒之間對某變數的訪問實際上是沒有依賴關係的,即一個執行緒不需要關心其他執行緒是否對這個變數進行了修改的。

2.TreadLocal的主要方法

public T get()
public void set(T value)
public void remove()
protected T initialValue()
  • set(T value)方法用於設定副本變數的值
  • get()方法用於獲取當前執行緒副本變數的值
  • initialValue()為當前執行緒預設的副本變數值。
  • remove()方法移除當前前程的副本變數值。

一、set(T value)方法

先來看一下原始碼:
在這裡插入圖片描述

①.getMap(t)方法

要設定副本變數值,就要先獲取此執行緒的ThreadLocalMap物件。
原始碼如下:

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

可以看到這個方法很簡單,只是返回當前執行緒的threadLocals變數

ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到在原始碼裡面,threadLocals變數的預設值是null。
所以我們可以得知,getMap(t)函式在當前執行緒第一次使用此方法的時候,返回的是null。

②.createMap()方法

此時我們走到了createMap方法,先看一下原始碼:

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

可以看到這時給threadLocals進行了賦值,再來看一下ThreadLocalMap(this, firstValue)方法是如何給threadLocals進行賦值的。

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

這是一個有參構造方法,傳入的值是當前正在呼叫set方法的ThreadLocal物件this,還有一個就是要設定的副本變數值。方法內部先建立一個鍵值對的物件陣列,然後傳入的當前TreadLocal物件通過hash方法得到需要儲存的下標i,然後將這個鍵值對儲存到此下標的陣列處,建立成功size賦值為1,並且給出此hash的負載因子。通過這個方法建立一個TreadLocalMap的物件賦值給threadLocals,此時通過threadLocals就可以找到這個鍵值對陣列。

③.map的set方法

如果在第二步已經進行過第一次的createMap方法,此時getMap方法就可以得到非null的map,可以呼叫map的set方法將新的鍵值對新增到threadLLocals裡面。這裡對原始碼不做贅述。

二、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();
    }

getMap()方法上面已經說過,如果還沒用呼叫set方法,直接呼叫get方法,map會得到一個null的值。

①.setInitialValue()方法

當map為空時,此函式會返回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);
        return value;
    }

這裡除了initialValue()方法其他上面都說過了,直接看initialValue()方法是如果得到value副本變數值的。

 protected T initialValue() {
        return null;
    }

恩,它很省事,直接返回一個null。那這樣導致setInitialValue()方法給get方法返回的也是一個null。
注意:如果ThreadLocal物件在沒有呼叫set()方法的時候,直接呼叫get()方法,會得到一個null,很可能產生空指標異常。

②.getEntry(this)方法

如果map不為空,我們自然要取得在當前執行緒的map(也就是treadLocals)裡面所儲存的和此TreadLocal對應的鍵值對,所以傳入this,看一下原始碼:

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

可以看到函式一開始就對此TreadLocal進行hash演算法,得到它所對應陣列下標i,然後判斷此下標處儲存的key是否和當前key一樣,如果一樣說明找到了正確的鍵值對,直接返回e。如果不一樣就呼叫getEntryAfterMiss(key, i, e)方法,這個方法原始碼裡面是使用線性探測的方法去找對應的key值,如果找不到就返回null。
如果返回了正確的Entry e,就將e的value值作為get的返回值。

三、remove()方法

原始碼如下:

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

直接呼叫了ThreadLocalMap的remove方法。

private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

這個方法也是先找到當前ThreadLocal對應的hash下標i,然後通過線性探測的方式去找準確的key值,如果找到了。就用clear()方法將這個key值置為null,方便之後的 expungeStaleEntry(i)方法進行清理,expungeStaleEntry(i)方法不止清理了i位置上的entry,還把i之後的key為null的entry都清理了,並且順帶將一些有雜湊衝突的entry給填充回可用的index中。

小總結

到這裡ThreadLocal的remove方法也分析完了。remove方法的關鍵就在於主動斷開entry的key的引用連結,這樣在後續的expungeStaleEntry方法中,就會將這種key為null的entry給設定為null,方便GC對記憶體進行回收。

ThreadLocal的set,get和remove方法看下來,除了正常的功能之外,就是多了對key為null的entry的清理工作,方便GC回收這部分佔用的記憶體。expungeStaleEntry就是最核心的清理方法,這也是ThreadLocalMap的一種防範機制,因為ThreadLocalMap的生命週期和執行緒是一樣長的,不採取這種防範機制,是會造成記憶體洩漏的。如果多定義了幾個ThreadLocal物件,並且執行緒都將佔用記憶體比較大的物件給放到對應的執行緒中,可能就會造成OOM異常了。

四、 initialValue()方法

這個方法在第二個get()方法那裡提到過,主要是當某個TreadLocal還沒有進行set方法而直接呼叫get方法,返回的預設副本變數值,初始是null。可以覆寫這個方法,改變初始值。

3.ThreadLocal的應用場景

最常見的ThreadLocal使用場景為 用來解決 資料庫連線、Session管理等。

相關文章