結合源碼談談ThreadLocal!


網上有很多關於ThreadLocal的介紹,有的介紹比較簡單,也有的介紹很復雜,比較難懂,今天,自己結合它的源碼,也做個簡易梳理,記錄如下!

ThreadLocal的作用

在多請求並發訪問過程中,我們往往需要將一個指定變量隔離起來,達到只對當前線程可用,其他線程不可用的效果,因此,我們就會使用到ThreadLocal來實現。

實現原理其實就是在每個線程中維護了一個Map結構(ThreadLocalMap,它是ThreadLocal中的靜態內部類),ThreadLocal對象為Key,需要隔離的值為Value。為了達到線程全局可用,我們往往將ThreadLocal聲明為全局靜態變量。

Thread中的ThreadLocalMap對象

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

那么ThreadLocal具體如何做到線程隔離的?我們下面做具體分析!

ThreadLocal

ThreadLocal的生命周期圖示如下:

我們暫不分析ThreadLocalMap,先單獨來看ThreadLocal的幾個方法源碼介紹!

1.對象初始化

ThreadLocal初始化比較簡單!

public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

我們往往在初始化時會給他指定一個默認值,不指定的話,默認值為null,這里有兩種指定方式:

第一種:直接復寫ThreadLocal中的initialValue方法
第二種:利用函數式編程,創建SuppliedThreadLocal對象,由get方法直接返回初始值

public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>(){
    @Override
    protected String initialValue() {
        return "Test";
    }
};
public static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(()->"Test");

SuppliedThreadLocal對象是對ThreadLocal的一個特定實現,通過構造函數傳入Supplier,再由實現的initialValue方法返回supplier.get()的結果,其他也沒什么可多介紹的。

2.獲取變量

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

從get方法我們可以看到,ThreadLocal是從當前線程中獲取到了ThreadLocalMap對象,然后取出其中的Entry.Value值,如果對象不存在就返回初始值,初始化方法initialValue會在這里調用一次,其他操作不再調用。

3.設置變量

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

set方法與get方法一樣,會通過當前線程取出ThreadLocalMap對象,然后將當前ThreadLocal對象作為Key,存儲Value值,ThreadLocalMap不存在時,會創建新的Map。

4.移除變量

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

移除變量同樣是根據currentThread來找到的Map,然后對當前ThreadLocal做remove操作。

ThreadLocalMap

通過ThreadLocal的操作介紹我們可以看到,ThreadLocal的操作都是基於ThreadLocalMap來實現的,所以,ThreaLocalMap才是我們對ThreadLocal變量實現線程隔離的重點。

1.Entry

ThreadLocalMap中存儲數據關系的是Entry,它的Key是ThreadLocal對象,采用弱引用,Value是一個強引用對象Object。當Entry.get()獲取的ThreadLocal為Null時,GC回收將直接清除該對象,但Value對象,需要我們手動清除,所以,我們需要在每個ThreadLocal調用結束時,執行remove方法,否則,有可能出現內存泄漏情況。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

那么,為什么ThreadLocal中沒有使用普通的Key-Value形式定義存儲結構呢?

因為如果這里使用普通的key-value形式來定義存儲結構,實質上就會造成節點的生命周期與線程強綁定,只要線程沒有銷毀,那么節點在GC分析中一直處於可達狀態,沒辦法被回收,而程序本身也無法判斷是否可以清理節點。弱引用是Java中四檔引用的第三檔,比軟引用更加弱一些,如果一個對象沒有強引用鏈可達,那么一般活不過下一次GC。當某個ThreadLocal已經沒有強引用可達,則隨着它被垃圾回收,在ThreadLocalMap里對應的Entry的鍵值會失效,這為ThreadLocalMap本身的垃圾清理提供了便利。

關於弱引用與強引用的關系以及他們的對象回收機制,這里不做過多介紹,有興趣的同學可以自行學習!

2.初始化

ThreadLocalMap的操作是基於Entry[]數組table完成的,數組初始化大小為16。table是一個2的N次方的數組,ThreadLocal通過AtomicInteger類型的nextHashCode,每次偏移HASH_INCREMENT=0x61c88647的大小來實現數據在數組上的平均分布。

Entry[]數組table為什么是一個2的N次方數組呢?

第一個原因是ThreadLocalMap使用的是開放定址法中的線性探測法,均勻分布的好處在於很快就能探測到下一個臨近的可用slot,從而保證效率。
第二個原因是位運算比取模效率高,rehash的時候只需要判斷0還是1。

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

關於Entry[]中如何解決碰撞沖突問題,可以參考:ThreadLocal 和神奇的數字 0x61c88647

3.獲取Entry

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

查詢table中的Entry值時,采用神奇的0x61c88647,ThreadLocal對象作為Key與Entry的Key相同時,返回此Entry,否則,采用開放定址法,從i開始線性探測查找Entry。

4.設置Entry

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

  for (Entry e = tab[i];
       e != null;
       e = tab[i = nextIndex(i, len)]) {
      ThreadLocal<?> k = e.get();

      if (k == key) {
          e.value = value;
          return;
      }

      if (k == null) {
          replaceStaleEntry(key, value, i);
          return;
      }
  }

  tab[i] = new Entry(key, value);
  int sz = ++size;
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
      rehash();
}

set方法中兼容新增與修改操作,如果找到同一個ThreadLocal對應的Entry時,則直接重新賦值Value,否則新建Entry賦值給table[i]。

5.移除Entry

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

remove操作同樣采用線性探測到指定的table[i],找到Key相同的ThreadLocal對象,然后通過指定弱引用的Key值為Null移除,並將table[i].value也置為Null,通過GC回收徹底刪除元素。

InheritableThreadLocal

ThreadLocal解決了線程隔離問題,但對於子線程想要獲取到父線程中的變量,又如何做呢?JDK為我們提供了另外一個線程本地變量實現類InheritableThreadLocal

InheritableThreadLocal繼承自ThreadLocal,與ThreadLocal一樣,它也會在Thread中定義一個Map結構來維護Entry訪問。

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

所以,每個Thread中都會保存有當前線程的ThreadLocal對象threadLocals,和繼承父線程的ThreadLocal對象inheritableThreadLocals。

那么,父線程數據如何傳遞給子線程的呢?我們來看Thread的init方法,是如何做的。

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
       ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

線程初始化時,會將父線程的ThreadLocalMap傳給子線程,通過Entry[]數組拷貝,完成子線程ThreadLocal對象的創建。具體操作在ThreadMap的另一構造方法完成。

private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

InheritableThreadLocal中重寫了ThreadLocal的三個方法:

childValue:獲取父線程變量值。
getMap:獲取繼承過來的ThreaLocal對象。
createMap:創建繼承父線程的ThreadLocal對象。

InheritableThreadLocal實現了Entry數組拷貝后,其他操作方法與ThreadLocal相同。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM