聊一聊執行緒變數綁定之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 會告訴你答案。